Avatar photo URL redirects: five hops, then image_not_fetchable
Sume follows up to five redirects for an avatar photo and re-checks every hop. A redirect to http or a private address returns 400 image_not_fetchable.
Sume follows an avatar photo URL through at most five redirects and validates every hop as if it were the original URL. If a hop points to http://, a private address, a URL with credentials or an explicit port, the answer is 400 image_not_fetchable with details.reason: unsafe_url. A sixth redirect ends in too_many_redirects, and a redirect response without a Location header ends in redirect_without_location.
Redirect handling matters because many photo URLs are not the file: they are short links, CDN rewrites or storage links that bounce to a signed address. The contract is on Create new avatar, read 2026-10-05.
The practical advice is short. Prefer the final file URL when you have it. If you must send a short link, test it with curl first, count the hops, and make sure the last response is a 200 with an image content type. A chain that works in a browser can still fail here, because a browser will happily follow a login redirect or an http hop that Sume refuses on purpose. Treat the five-hop count as a budget, and spend as few of them as you can.
How the hop loop behaves
Sume fetches with manual redirect handling and treats 301, 302, 303, 307 and 308 as redirects. Each Location is resolved against the current URL, so relative locations work. Then the public-HTTPS rules from the URL validation post run again on the new target.
- Follows 301, 302, 303, 307 and 308.
- Resolves relative Location headers against the current URL.
- Allows five redirects; the sixth is too_many_redirects.
- Rejects a hop to http, a private host, credentials or a port with unsafe_url.
| Case | details.reason | Result |
|---|---|---|
| Redirect to a public HTTPS image | none | Followed |
| Redirect to http://127.0.0.1/internal-photo.png | unsafe_url | 400 image_not_fetchable |
| Redirect that has no Location header | redirect_without_location | 400 image_not_fetchable |
| More than five redirects | too_many_redirects | 400 image_not_fetchable |
| Location that is not a valid URL | invalid_url | 400 image_not_fetchable |
The redirect to a private address
A test sends a first request to https://assets.example.com/photo.png that answers 302 with Location: http://127.0.0.1/internal-photo.png?token=private. The API returns 400 image_not_fetchable with url_host: assets.example.com. The response body does not contain 127.0.0.1 or the token, so neither the internal target nor the secret leaks into logs that quote the error.
Count the hops yourself
Find the final address and the number of hops with curl, then give Sume the final URL if your link chain is long.
curl -sS -L -o /dev/null --max-redirs 10 \
-w 'final=%{url_effective} hops=%{num_redirects} type=%{content_type}\n' \
'https://short.example.com/headshot'
# hops above 5 fails in Sume; a final http:// or private host fails tooWhy a hop cap and a clock both apply
Bouncing links are routine. A photo in a shared folder often has a share URL that redirects to a storage URL, which redirects to a signed one. Two or three hops is normal and works. The cap of five exists so that a redirect loop ends quickly: a loop between two URLs would otherwise fetch until the 15 second abort.
Each hop is a real request against your host, so a chain of five slow hops can run into the time limit before it hits the hop limit. When that happens the error is image_not_fetchable with no status, which looks like a timeout, because it is one.
What the per-hop check stops
Re-checking every hop protects against an open redirect on a trusted host. Without it, a public URL that bounces to an internal address would let a caller point Sume's fetcher at private services. The same check means your redirect target cannot use an explicit port either, so a final https://cdn.example.com:8443/photo.png fails even if the first URL was clean.
What to send
If the final URL is stable, send it. If it is a signed link that expires, keep the short link and make sure the chain stays under five hops and ends on HTTPS. Do not send a link whose last hop is a login page: the final response must carry an image content type, and a login page will fail with unsupported_image_type.
Sources
Related posts
More in Developers
- Avatar submit response: what next_action tells your client to do
next_action has three values on avatar submits: poll_status while queued or processing, fetch_result once completed, inspect_events for failed or canceled jobs.
- Avatar sync wait: timed_out vs capacity_exhausted, and what to do
A sync avatar submit can return 2xx with sync.timed_out or sync.capacity_exhausted true. Both mean keep the job: poll status_url, never submit a new paid job.
- Avatar script too long? The 4 to 60 second window and how to split it
Sume accepts an avatar talking video only when the script estimates 4 to 60 seconds. Split longer scripts into scenes or jobs; costs from $0.74 to $33.
- Avatar video 400: script must include at least one word
A Sume talking-video request with an empty or whitespace-only script returns 400 invalid_request, with no job or ledger entry. Guard blank template fields.
Written by Sume