HTTP contract

This is the public interface. Changes to anything marked stable require a new catalog major version and a changelog entry; the old behaviour keeps working.

1. URL scheme (stable)

https://loremfile.dev/{format}/{name}

Descriptor grammar

Descriptors are ordered tokens separated by -:

[variant]-[dimension|duration|count|size]-[qualifiers]
Token type Examples Rule
dimension 640x480, 1080p, 4k, a4, letter Width×height in pixels, or a well-known label
duration 5s, 30s, 10min Seconds or minutes
count 3pages, 10rows, 100k-rows, 1000-files, 10frames Unit spelled out
size 1kb, 10mb, 100mb (decimal) · 1kib, 10mib, 100mib (binary) Exactness class from catalog
boundary 10mib-plus-1, 10mb-minus-1 Exactly nominal ± 1 byte
encoding utf8-bom, utf16le, latin1, crlf Text fixtures
codec/profile vp9, h264-baseline, 320kbps, 24bit-96k Media fixtures
dataset people-1000, orders-100k Shared synthetic datasets

2. Methods (stable)

Method Behaviour
GET Returns the object. Supports Range, If-None-Match, If-Modified-Since.
HEAD Same headers as GET, no body.
OPTIONS CORS preflight, answered per bucket CORS policy (§5).
Others Not part of the contract; Cloudflare/R2 return 4xx/405.

3. Status codes (stable)

Code When
200 Object found
206 Valid Range satisfied
304 Conditional request matched
301 www. host → apex; (Phase 3 only) /random/*
404 Unknown key. Body is a short plain/XML message from R2 and is not part of the contract. R2 sends no Cache-Control on 404, and 404s are cached — verified in M2.4 probe runs 7-9, a non-zero Age on the first sample of every run. edge_ttl.mode: respect_origin (08 §5.4) means use the origin's header if present, otherwise Cloudflare's default caching behaviour, and the default for 404/410 is 3 minutes. This row said exactly that, was "corrected" to not cached, and the original was closer to right; the correction rested on MISS/MISS readings of cf-cache-status, which is not evidence, because a MISS means only that this edge node had not seen it. What is verified is that they are cached — the duration is not: Age 3s is the age at sampling, not a TTL, and the 3-minute figure is Cloudflare's documented default rather than something measured here. Caching them by status code instead was considered and rejected (ADR-026, whose conclusion does not rest on this). Whether R2 bills a 404 as a Class B read is separately unmeasured and needs loremfile usage (M4.3).
416 Unsatisfiable range
429 Rate limited (300 requests / 10 s per IP). Retry after 10 s.
403 WAF managed rule matched (only for exploit-shaped requests)

4. Response headers

4.1 On every fixture (stable)

Header Value Source
Content-Type Exact MIME from the catalog, e.g. application/pdf, text/csv; charset=utf-8, application/vnd.apache.parquet Object metadata set at upload
Content-Length Exact byte count (matches manifest.bytes), because no-transform disables edge compression for fixtures. R2
Cache-Control public, max-age=31536000, immutable, no-transform Object metadata
Content-Disposition inline; filename="{last path segment}" Object metadata
Accept-Ranges bytes R2
ETag Opaque. Do not assume it is an MD5; use manifest.sha256. Demonstrated rather than warned about since 2026-09-10: csv/people-100k.csv is uploaded multipart and serves "0fb5712e2d6fa8cae82c86acc1e2dabf-2", while single-part objects serve a plain 32-hex digest. Two shapes, same header. R2
Last-Modified Upload time; informational R2
Access-Control-Allow-Origin * (only when request has Origin) Bucket CORS policy
Access-Control-Expose-Headers Content-Length, Content-Range, Content-Type, Content-Disposition, ETag, Accept-Ranges, Last-Modified Bucket CORS policy
X-Content-Type-Options nosniff Response header rule H1
Cross-Origin-Resource-Policy cross-origin Rule H1
Timing-Allow-Origin * Rule H1
X-Robots-Tag noindex Rule files_noindex (raw files stay out of search results; pages are indexed instead). It reaches every path containing a dot except four display assetsfavicon.ico, apple-touch-icon.png, /assets/og.png, /assets/mark.svg — which exist to be shown by search engines and social platforms and so carry no X-Robots-Tag (2026-09-21; they keep nosniff, CORP and TAO from H1). manifest.json, sitemap.xml, robots.txt, llms.txt, llms-full.txt and security.txt still carry noindex, and that is accepted — see 04 §7 before changing it.
cf-cache-status HIT/MISS/… informational Cloudflare

4.2 Additionally on active-content fixtures (.html, .htm, .xhtml, .svg, .xml) (stable)

Header Value Why
Content-Security-Policy sandbox; default-src 'none'; img-src https://loremfile.dev data:; media-src https://loremfile.dev; style-src 'unsafe-inline'; font-src https://loremfile.dev sandbox makes the document inert when opened directly: no script execution, no form submission, no plugins, opaque origin. The explicit host allow-list (not 'self', which is meaningless for an opaque origin) lets the fixture's own images, media, inline styles and fonts still render so people can eyeball it. Scripts stay blocked because sandbox forbids them and no script-src is granted.

The sandbox CSP is deliberately not applied to PDFs or media: browsers' built-in PDF viewers have failed to render PDFs served with a restrictive CSP (Chromium issue 40328564, Mozilla bug 1582115), so PDFs carry no CSP at all.

4.3 On site pages (extensionless paths) (may evolve)

Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none', X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, X-Content-Type-Options: nosniff, Permissions-Policy: camera=(), microphone=(), geolocation=(), Link: </llms.txt>; rel="describedby", </.well-known/api-catalog>; rel="api-catalog" (04 §11; also on /.well-known/api-catalog itself, whose HEAD must carry it — RFC 9727 §2).

Additionally on /legal/imprint and /legal/privacy, and on their trailing-slash forms: X-Robots-Tag: noindex, nofollow, nosnippet (header rule legal_pages_noindex, 08 §5.3), and the same directive as <meta name="robots"> in the HTML (ADR-016 amendment, ADR-028). No other site page carries an X-Robots-Tag.

5. CORS (stable)

Bucket CORS policy (S3 syntax). It is applied once by the owner with an admin token (08 §2 step 7 and §7) because bucket configuration needs the R2 admin permission that neither CI token has; infra audit verifies the live behaviour:

[
  {
    "AllowedOrigins": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["Content-Length", "Content-Range", "Content-Type", "Content-Disposition", "ETag", "Accept-Ranges", "Last-Modified"],
    "MaxAgeSeconds": 86400
  }
]

Consequences: fetch() from any origin works; <video>, <audio>, <img crossorigin> work; Range requests from browsers work (the Range request header is covered by AllowedHeaders: *); sites using Cross-Origin-Embedder-Policy: require-corp can embed fixtures because of Cross-Origin-Resource-Policy: cross-origin.

6. Caching semantics

7. Immutability policy (stable)

  1. A fixture path, once published to R2, is not rewritten or reused. The rule is about fixtures and nothing else: probe --down deletes everything under _probe/, upload --site overwrites the site keys on every deploy, and _locktest/probe exists precisely to be written once and then refused. None of those is a fixture, none is covered by this promise, and the bucket lock rules draw the same line at the storage layer — every format prefix is locked, _probe/ and the site keys are not. Entry into manifest.json on main is not the boundary either — the promise ADR-005 makes is to embedded URLs and to hashed fixtures in other people's tests, and both require the bytes to have been served. An entry that never reached the bucket has no consumer and breaks no promise, so it may be removed outright rather than tombstoned; a tombstone means "was published, now removed" and would be a lie on the format page. This is not a relaxation: R2 bucket locks already implement exactly this boundary at the storage layer (ADR-023), and the wording above claimed something stricter than the system has ever enforced. Amended in M3.6, when five entries described bytes that had never been published and could not be regenerated.
  2. Its bytes, sha256, bytes, mime are frozen from publication onward. CI enforces the manifest half (lock check); R2 bucket locks enforce the storage half.
  3. To fix a defective fixture, add a new fixture (e.g. a4-3pages-v2.pdf), set supersededBy on the old entry, mark it deprecated: true, keep serving it.
  4. Only a legal takedown may remove an object; the manifest entry then becomes {"status": "removed", "reason": "…", "removed_at": …} and the site marks it.
  5. The manifest.json document itself, sha256sums.txt, the site and index.json files are mutable.

8. Client guidance (published on the site)

9. Examples

# 10 MiB + 1 byte, to test a "10 MiB max" upload limit
curl -fsSLO https://loremfile.dev/bin/10mib-plus-1.bin

# HEAD for size
curl -sI https://loremfile.dev/pdf/a4-3pages.pdf | grep -i content-length

# Range
curl -s -r 0-99 -o part.bin -D - https://loremfile.dev/mp4/720p-5s.mp4 | grep -i content-range

# Verify
curl -fsSL https://loremfile.dev/sha256sums.txt | grep 'pdf/a4-3pages.pdf' | sha256sum -c