Someone on V2EX wrote up a workflow a lot of us have quietly built some version of: one command that generates an image, saves it locally, uploads it to an image host, and spits back a public CDN link ready to paste into an article. Generate with an async task API (POST a prompt, poll for success, download the result), hand the file to a local PicList HTTP service on `127.0.0.1:36677`, get back `{"success":true,"result":["/ai-market-guide/market/7554/ Clean. Scriptable. The kind of thing you let an AI assistant write in a few lines and never think about again.
Then the link was dead.
Upload reported success. The returned URL came back with `HTTP 441` and `server: Parking/1.0` — a parking page, not an image. Two things had stacked up. The custom domain on the image host had quietly expired and turned into a parking page, which meant every image posted through it historically was also dead, and the author had no idea. On top of that, the local machine was running Clash in TUN / fake-ip mode, so the `cdn.*` domain resolved to a `198.18.0.x` reserved address. Local `curl` and even `dig @8.8.8.8` were hijacked and useless. The only way to learn the truth about that URL was to request it from an exit that didn't route through the local proxy.
The real failure is trusting the success flag
The lesson buried in that post is worth pulling out: `success: true` from an upload API only tells you bytes were accepted and a string was returned. It says nothing about whether that string resolves to a live, image-serving URL on the public internet. The two are completely different questions, and the gap between them is exactly where silent rot lives.
This isn't unique to one host. Any pipeline that chains generation to an upload step inherits the same blind spot. Custom domains lapse. CDNs reconfigure. A path works today and 404s in three months. If your only check is the success boolean at write time, you find out when a reader does, or when you reopen an old article and the covers are gone.
The fake-ip detail makes it worse in a specific, instructive way. The place you'd naturally run the check — your own laptop, right after upload — can be the one place that lies to you. You need a vantage point outside your local network stack to get an honest answer about what the public actually sees.
Build the durable half as a hosted step
The generation and upload parts are fine where they are. The part worth hardening is verification, and that's a small, boring, well-defined job: given a URL, fetch it from somewhere that isn't your machine, and decide whether it's really an image.
A hosted Python step covers this cleanly. You can run Python online so the fetch originates off your local box, which sidesteps the fake-ip trap entirely — there's no Clash tunnel between the verifier and the open internet, so what it sees is closer to what a reader sees. The logic is nothing clever:
- Request the returned URL and read the status code. A `441`, a `403`, a redirect to a parking host — all fail.
- Check the `Content-Type`. If it claims `text/html` when you asked for an image, fail.
- Read the first bytes and confirm a real image signature (PNG, JPEG, WebP magic numbers) rather than trusting headers alone.
- Watch the `server` header for tells like `Parking/1.0`.
Only a URL that passes all of that gets the verdict `ok`. Everything else gets a reason string, not a shrug.
One honest boundary: a hosted verifier removes your local proxy from the picture, but it's still one network vantage point, not a global one. Geo-restricted or intermittently flaky hosts can still look different from somewhere else. That's a real limit, not a bug to paper over — treat the verdict as "this URL served a real image when checked from here, at this time," which is already far more than the success flag gave you.
Log every verdict to an inspectable ledger
A pass/fail at the moment of upload is useful. A record you can query later is what actually catches silent rot. Write every verdict to a SQLite ledger: the URL, the timestamp, the status code, the detected content type, the first-bytes verdict, and the final `ok` / reason. SQLite is a good fit here because it's a single file you can keep, re-run against, and open directly in a SQLite editor when you want to eyeball what rotted and when.
That ledger does two jobs. At write time it blocks a bad link from reaching your article. Over time it becomes the thing you re-sweep. Point the same verifier at every historical URL in the table on a schedule, and the day a domain lapses into a parking page you get a row flipping from `ok` to `441 Parking/1.0` — instead of discovering it a quarter later. The V2EX author found their old images were all dead by accident; a ledger turns that accident into a query.
Keep the schema flat. One row per check, not per URL, so you can see a URL's history — the moment it stopped serving is usually more informative than its current state.
Give it a surface and a schedule
To make this something your pipeline actually calls rather than a script you forget, give it a small HTTP surface via web app hosting: a `POST /verify` that takes a URL, runs the checks, writes the ledger row, and returns the verdict. Your existing generate-and-upload command calls it right after the upload step and refuses to paste a link that didn't pass. The same endpoint, pointed at your stored URLs on a timer, becomes the background rot-sweeper.
One thing to flag honestly: an endpoint like this, left open, is an unauthenticated fetcher anyone can point at arbitrary URLs. Put a token check in front of it before you expose it, and be deliberate about what hosts it's allowed to fetch, so it doesn't become a request-forwarding tool for someone else.
Why this is worth the small effort
The appeal of the one-command flow was never speed for its own sake — it was not having to babysit. But a flow that silently emits dead links isn't actually unattended; it just defers the babysitting to future-you, at a worse time, with worse information. Moving verification into a hosted step with a durable ledger closes that gap for roughly the cost of the few lines the original upload script took.
The generation model, the image host, the upload tool — swap any of them. The verifier doesn't care what produced the URL or who's serving it. It only answers the one question the success flag never did: is there really an image at the other end, right now, as seen from outside your own machine. That's the half worth owning.