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.

4 min readSume
All posts

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.
Redirect cases and results, read 2026-10-05
Casedetails.reasonResult
Redirect to a public HTTPS imagenoneFollowed
Redirect to http://127.0.0.1/internal-photo.pngunsafe_url400 image_not_fetchable
Redirect that has no Location headerredirect_without_location400 image_not_fetchable
More than five redirectstoo_many_redirects400 image_not_fetchable
Location that is not a valid URLinvalid_url400 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 too

Why 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

All Developers posts

Written by Sume