Invariants: RomM HTTP¶
How a RomM answer is classified — a 404's authority above all — the one choke point every RomM request goes out through, custom proxy headers, and the minimum RomM version. How an entry reads and binds, and the other areas: the register's index.
-
A definitive 404 is
not_found, neverserver_unreachable—classify_error's table answers every not-found classnot_found— check + testAnd a catch-all
except Exceptioninservices/may neither bind a verdict key (reason/status/recommended_action) to a hardcodedSERVER_UNREACHABLEnor raiseServerUnreachableor a refusal whose reason is that slug; let the RomM error reach the entrypoint or route it throughclassify_error, or peel the 404 off with a siblingexcept RommNotFoundErrorwhere the verdict is a status or a partial-success flagscripts/check_404_not_unreachable.py --check(unseen: a verdict or refusal a helper builds or a variable holds,ServerUnreachableunder another name, the slug held in a variable, or in a constant other than an attribute namedSERVER_UNREACHABLE) andtests/lib/test_errors.py::TestEveryNotFoundClassAnswersNotFound(RommNotFoundErrorand every class defined in or imported intolib/errors.pyderiving from it; unseen: onelib/errors.pyneither defines nor imports, and a 404 that is noRommNotFoundError— the pairing-code exchange's two 404 errors answerserver_unreachablein the table (the connection service catches both first)) - A 404 becomesRommNotFoundErroronly when RomM's entity layer is proven to have answered it; only the three byte-stream fetches opt out — test + prompt-only —TestNotFoundDiscriminationplus the per-call-sitetest_generic_route_404_still_raises_not_foundtrio intests/adapters/romm/test_http.py; a fourth byte-stream fetch's opt-out is prompt-only —.claude/rules/romm-http.md- Every RomM request goes out throughRommHttpAdapter._urlopen, the single point that clears the known-unreachable state — check — A request method callingurllib.request.urlopenitself leaves every retry ladder degraded to one attempt until an unrelated path happens to succeed, and nothing else fails.scripts/check_urlopen_choke_point.py(structural, AST call sites — an aliasedurlopenor agetattrslips past it. Which requests may skip the ladder, and which passromm_origin=Falsebecause they do not talk to RomM at all, stays prompt-only in.claude/rules/romm-http.md) - Tender attaches a configured custom header to a RomM-origin request and to no other request it issues, and never over a header the adapter sets itself — test + prompt-only —tests/domain/test_custom_headers.pypins the validation in both directions (every reserved name case-insensitively, a CRLF in a value, and what the persisted reading skips), andtests/adapters/romm/test_http.py::TestCustomProxyHeaderspins the three attachment points, the one exclusion, and that a hand-plantedAuthorization/Hoststill loses. Nothing joins them. The rule spansdomain/custom_headers.py, the transport's_apply_origin_headersand its three callers —_apply_default_headers(every authenticated route),unauthenticated_post_json(the pairing-code exchange) andbasic_auth_request(the token mint) — plus the one place that must NOT call it,download_external. Both directions fail in silence and each one is worse than it looks. A fourth request method that forgets the helper works perfectly for the user who has no proxy and 403s for the user who has one, on that path only. Adding it todownload_externalhands the user's proxy credential to a third-party metadata CDN, which no test would notice because the fetch still succeeds. What Tender issues is the whole of the claim:_urlopenuses the default opener, so urllib's redirect handler follows a 30x by copying every header butcontent-length/content-typeonto the next request with no same-origin test — measured, not read: a cross-host 302 delivers both the configured header and the RomM bearer to the foreign host. That is the transport's behaviour and predates this rule (the bearer always travelled it), which is why the invariant is worded about attachment rather than about arrival; #1889 holds the gap. The reserved set is held by two mechanisms, not three, and they are not equally strong. One is validation —_name_refusalagainstRESERVED_NAMES, reached from bothresolve_custom_headers(the wire) andstored_custom_headers(every request, so a hand-editedsettings.jsoncannot route around it). Those are two call sites of ONE frozenset: drop a name from it and both gates open in a single edit. The other is attachment ORDER, and it covers only a name the adapter itself re-adds after_apply_origin_headers—User-Agent,Authorization,Content-Type, and conditionallyAccept-Encoding/Range/If-None-Match/If-Modified-Since. It coversHostandContent-Lengthnot at all, because the adapter sets neither:http.client._send_requestsuppresses its own derivedHostwhen the caller supplied one, so a configuredHostretargets every request's virtual host — and for that name, the one this list singles out as dangerous, the frozenset is the only defence there is. Nothing pins the ordering leg either: the two tests that look like they do (..._never_displaces_the_bearer,..._never_retargets_the_request) pass becausestored_custom_headersdrops the entry long beforeadd_headeris reached, so both stay green if the ordering is reversed. Detail:docs/architecture/backend-architecture.md→ "the headers every RomM-origin request carries" - Every RomM minimum stated for a reader matches the enforcedMIN_ROMM_VERSION— check —scripts/check_romm_min_version.py. The constant inbackend/domain/identity.pyis the floortest_connection()refuses a server below; every other place the number appears is a restatement for a reader, and a restatement drifts. The check holds exactly the statements itsCLAIMSlist names — each one a narrow regex that captures the version and nothing around it, so--fixcan rewrite it in place — and fails both when a named statement says another number and when it no longer matches at all, because a regex that silently stopped matching would read as a claim that holds. What it cannot see is a restatement nobody added to that list: a new page stating the floor is unchecked until its sentence is listed there. The worked examples of the floor split three ways. The ones above the floor (5.4.2-beta,5.5.0-alpha.1) are unchecked, because no equality test fits them, and a floor raise makes them false, so they are rewritten by hand. The ones at the floor (5.4.1-beta.1,5.4.1-alpha.1) are listed and checked where a page calls them the floor's own tags, as both save-sync pages do. The conditional one in the ConnectionService notes of backend-architecture.md is not listed, because it stays true whatever the floor is. ADRs are out of scope: they record the floor as it stood when the decision was taken, so the numbers in them are history and must not be rewritten