Direct answer: in the 36 article pages cached for study 83, 188 external images came from only two hosts; one of them (H1) served 149 images across six sites. Between 18:42 and 18:44 on 2026-11-06 we sent six request profiles to six image URLs on H1 (bare request, Chrome UA, plus Accept, plus Referer, plus Sec-Fetch, and HEAD): all 36 returned HTTP 567 with zero bytes. Six requests to the control host C1 all returned 200 and image/png. Changing headers did not unlock H1.
A cross-day recheck at 00:40–00:42 on 2026-11-07, about six hours later, kept the same status split; details are in the cross-day section below.
A browser check did not reproduce "the browser can display it" either: opening the same image URL in Chromium returned an HTML 403 challenge page, and opening the source article page showed the logo and body images as broken placeholders. This finding belongs to this machine, network egress and time window only.
Method: discover first, then fix the variables
We reused the 36 cached article pages from study 83 (10 sites, fetched 2026-11-03) and parsed static <img src> only, excluding data:/blob:/javascript: and lazy data-src-only tags. External images were grouped by host; the first URL per host received one bare request (Python urllib default UA), which produced one 567 host and one 200 host.
The treatment group was one URL per affected site on H1 (six URLs); the control was one URL on C1. Each URL received six request profiles, with 1.2 s between requests to the same host, a 20 s timeout and 42 requests in total. We recorded status, Content-Type, Content-Length, bytes read and Server only; no CDN policy was inferred.
| Item | Rule | Boundary |
|---|---|---|
| Image | img with a non-empty, non-data: src | CSS backgrounds excluded |
| External | Registrable domain differs from the article page | Subdomains count as internal |
| H1 / C1 | External host returning 567 / 200 to a bare request | This machine and request only |
| Available | 200, image/*, non-zero bytes read | Copyright and long-term availability not judged |
Result: six profiles, 36 responses, all 567
| Profile | Headers / method | H1 (6 URLs) | C1 (1 URL) |
|---|---|---|---|
| V0 | Bare request (urllib default UA) | 6 × 567 | 1 × 200 |
| V1 | Chrome UA | 6 × 567 | 1 × 200 |
| V2 | + Accept: image/… | 6 × 567 | 1 × 200 |
| V3 | + Referer: source article | 6 × 567 | 1 × 200 |
| V4 | + Accept-Language / Sec-Fetch-* | 6 × 567 | 1 × 200 |
| V5 | HEAD (same headers as V4) | 6 × 567 | 1 × 200 |
| Total | 6 profiles × 7 URLs | 36 × 567 | 6 × 200 |
In the first pass the 36 H1 responses recorded no Content-Type, Content-Length or Server header and read zero bytes (the 567 error body was not read then; see the recheck below). The six C1 responses were all image/png, Content-Length 12,017 bytes, Server openresty. The same probe works on the control host, so the 567 is not a bug in the script.
Browser check: "the browser can display it" did not reproduce
| Check | Result | Note |
|---|---|---|
| Chromium opens the image URL directly | HTML 403 challenge page | Page text "http 403." plus a numeric token, not an image |
| Chromium opens the source article page | Logo and body images show placeholders | Same machine and network; the site's own images did not render |
| 36 Python requests | All 567 | Including full browser headers and HEAD |
Study 83 recorded that "a browser can usually display these, while a script cannot get the bytes". In this recheck at 18:43–18:44 on 2026-11-06, neither direct access nor the source page rendered the images. The two observations disagree, which shows that this behaviour can change with network egress, time or CDN policy; one "it works in my browser" is not evidence that the image is available to everyone.
Cross-day recheck: 00:40–00:42 on 2026-11-07
About six hours after the first experiment, across midnight, we reran the same six request profiles against the same six H1 URLs and one C1 URL. All 42 statuses were unchanged: H1 was still 36/36 = 567 and C1 still 6/6 = 200. Opening the same image URL directly in the browser still returned an HTML 403 challenge page.
The recheck changed how error responses are read: the first pass recorded only the status when it saw a 567 and did not read the body or headers; the recheck reads the HTTPError body and Content-Type. As a result, the 36 H1 567 responses now carried a 29–30 byte text/markdown body (GET; HEAD returned zero bytes), containing "http 403." plus a numeric token. The token may differ per request; this does not establish CDN policy, only that the 567 is not an empty response.
| Item | 2026-11-06 18:42–18:44 | 2026-11-07 00:40–00:42 |
|---|---|---|
| H1 status | 36 × 567 | 36 × 567 |
| C1 status | 6 × 200 | 6 × 200 |
| H1 body | Not read (recorded as 0 bytes) | 29–30 B, text/markdown (GET) |
| C1 body | 12,017 B, image/png | 12,017 B, image/png |
| Direct browser access | HTML 403 challenge page | HTML 403 challenge page |
Conclusion: across this roughly six-hour cross-day window the 567/200 split was stable; what changed is that this pass finally read the 567 body. "We read a body this time" must not be written as "the server behaviour changed".
Host and site distribution
| Host | Sites | Image URLs | Bare-request status |
|---|---|---|---|
| H1 | 6 | 149 | 567 |
| C1 | 1 | 39 | 200 |
| Total | 7 | 188 | — |
The 36 pages come from 10 sites; seven of them carry at least one external image. H1's 149 images are spread across six sites, so this is not a single-page incident. The 39 extension-less images on E16 all come from C1 and were retrievable this time.
What this means
- 567 is not a broken image. It is an HTTP response; the same URL may work on another network or client. The public report only says "this machine did not get bytes".
- 567 is not a header problem here. At least from this vantage point, UA, Accept, Referer, Sec-Fetch and HEAD are not switches.
- Record two columns in acceptance. "Script request status" and "browser rendering" are different facts; testing only one misleads.
- External images need a fallback. CDN host, edge policy or regional access changes hit the page directly; critical images should be self-hosted or have an alternative source.
- Do not use HEAD as a substitute for GET. A HEAD pass does not prove GET returns bytes; here they agreed on H1, but that is not a general rule.
Relationship to the image-delivery and alt studies
Study 83 counted 188 external images (37.7%), and study 85 sampled 30 images of which 13 returned 567. This study isolates the question "is 567 caused by request headers?" and finds 6 URLs × 6 profiles all 567, with no browser rendering either. Together, the safer conclusion is that external-image availability must be confirmed page by page in a real browser; an extension or a single script request cannot settle it.
Limits
- One host, small sample: one 567 host, six URLs and six profiles; not a within-host distribution and not an industry benchmark.
- One egress, two time windows:18:42–18:44 on 2026-11-06 and 00:40–00:42 on 2026-11-07, about six hours apart; no 24-hour, multi-region or alternate-egress recheck.
- One Chromium instance: not evidence for other browsers, devices or logged-in states.
- No site session: no cookies, login or in-site navigation; only Referer was tested.
- Anonymised: the public CSV uses H1/C1 and sample_id; host names and full URLs are withheld, so it cannot be replayed row by row externally.
Data and reproduction
Anonymised data: equipment-image-cdn-header-response-2026.csv (42 rows × 12 columns: host, sample, profile, method, status, Content-Type, length, bytes read, Server, error and time). In-repo copy: docs/research/2026-11-image-cdn-header-response.csv.
Cross-day data: equipment-image-cdn-header-recheck-2026.csv (42 rows × 12 columns, 00:40–00:42 on 2026-11-07); equipment-image-cdn-header-compare-2026.csv (42 rows × 11 columns, T0/T1 status and bytes-read comparison). In-repo copies: docs/research/2026-11-image-cdn-header-recheck.csv and 2026-11-image-cdn-header-compare.csv.
Probe: scripts/image_cdn_header_probe.py; tests: scripts/test_image_cdn_header_probe.py (no network, 12 tests covering six header profiles and six response classes). Run:
python -m unittest scripts.test_image_cdn_header_probe
python scripts/image_cdn_header_probe.py --url <image URL> --page-url <source page> --out response.csv
Related: the article image-delivery pilot, the image inventory checker, and the article image-attributes pilot.
Sources and definitions
- Pages and image URLs: raw HTML of the 36 article pages cached for study 83 (fetched 2026-11-03, 10 sites).
- Discovery and experiment: 2026-11-06 18:42–18:44, Python urllib, 42 requests, 1.2 s apart.
- Browser check: 2026-11-06 18:43–18:44, local Chromium, one direct visit and one source-page visit.
- Cross-day recheck: 00:40–00:42 on 2026-11-07, same 7 URLs and 6 profiles, 1.2 s apart; one direct browser visit.
- Statuses are recorded as returned; 567 is a non-standard code and is not attributed to a specific CDN policy.