Tracking the producer side of CannObserv/cannobserv#271 (enriched
BlobAvailableEvent). Consumer: CannObserv/watcher#241 (Phase 4).
Why
src/worker/handler.py holds a full FetchResult at publish time and puts six of its
facts on the floor. Watcher needs them after Phase 4 because it no longer performs the
fetch itself:
final_url — Watcher derives effective_url and domain_name (its rate-limiter
key, and an FK) from the redirect chain. Post-cutover it sees only the requested URL.
Also blocks watcher#157.
content_type_raw — Watcher stores the verbatim Content-Type header
(watcher#168). The normalized media_type drops charset and defaults absent headers to
application/octet-stream, which is exactly the value Watcher's dispatch reads as
"unknown, guess from the URL". Raw passthrough (None when absent) preserves the
distinction.
status_code, fetched_at — occurred_at is stamped at publish, which under
retry or PEL parking is not when the bytes came off the wire.
etag, last_modified — the conditional-GET seam. Returned here and replayed via
ContentFetchCommand.headers (CannObserv/cannobserv#272), Replicator gets conditional
GET while holding zero per-URL history — which is the point: this service has no
database and should not grow one.
Scope
Populate the new optional fields in handler.py's BlobAvailableEvent construction from
the FetchResult. Notes:
final_url — only when the client actually reports a landing URL; do not synthesize it
by echoing command.url, or an issuer cannot distinguish "no redirect" from "unknown".
content_type_raw — verbatim, None when the header was absent. The existing
_media_type normalization is unchanged; this adds a channel beside it.
last_modified — pass the string through unparsed; it gets replayed verbatim in an
If-Modified-Since and a parse/re-serialize round trip can differ from what the origin
sent.
- Log-line dedupe:
duration_ms is currently called out in handler.py as "the one
number nothing else keeps". With fetched_at on the fact that comment is stale — worth
a look, though duration_ms itself is still journal-only.
Follow-on to flag, not fix here
A body-less 304 currently dead-letters. Conditional GET is only half-usable until that
becomes a first-class outcome — reason="not_modified" on the fetch_failed fact
(CannObserv/replicator, companion issue), or a dedicated content_unchanged fact. Decide
before an issuer sends its first validator header.
Contract doc
Update the BlobAvailableEvent field table in
docs/contracts/content-fetch-issuer-contract.md in the same change ("edit one, edit
both"), including the "unknown vs. absent" semantics for final_url and
content_type_raw — that distinction is the whole value of the fields and will be lost if
only the types are documented.
Refs
Tracking the producer side of CannObserv/cannobserv#271 (enriched
BlobAvailableEvent). Consumer: CannObserv/watcher#241 (Phase 4).Why
src/worker/handler.pyholds a fullFetchResultat publish time and puts six of itsfacts on the floor. Watcher needs them after Phase 4 because it no longer performs the
fetch itself:
final_url— Watcher deriveseffective_urlanddomain_name(its rate-limiterkey, and an FK) from the redirect chain. Post-cutover it sees only the requested URL.
Also blocks watcher#157.
content_type_raw— Watcher stores the verbatimContent-Typeheader(watcher#168). The normalized
media_typedrops charset and defaults absent headers toapplication/octet-stream, which is exactly the value Watcher's dispatch reads as"unknown, guess from the URL". Raw passthrough (
Nonewhen absent) preserves thedistinction.
status_code,fetched_at—occurred_atis stamped at publish, which underretry or PEL parking is not when the bytes came off the wire.
etag,last_modified— the conditional-GET seam. Returned here and replayed viaContentFetchCommand.headers(CannObserv/cannobserv#272), Replicator gets conditionalGET while holding zero per-URL history — which is the point: this service has no
database and should not grow one.
Scope
Populate the new optional fields in
handler.py'sBlobAvailableEventconstruction fromthe
FetchResult. Notes:final_url— only when the client actually reports a landing URL; do not synthesize itby echoing
command.url, or an issuer cannot distinguish "no redirect" from "unknown".content_type_raw— verbatim,Nonewhen the header was absent. The existing_media_typenormalization is unchanged; this adds a channel beside it.last_modified— pass the string through unparsed; it gets replayed verbatim in anIf-Modified-Sinceand a parse/re-serialize round trip can differ from what the originsent.
duration_msis currently called out inhandler.pyas "the onenumber nothing else keeps". With
fetched_aton the fact that comment is stale — wortha look, though
duration_msitself is still journal-only.Follow-on to flag, not fix here
A body-less 304 currently dead-letters. Conditional GET is only half-usable until that
becomes a first-class outcome —
reason="not_modified"on thefetch_failedfact(CannObserv/replicator, companion issue), or a dedicated
content_unchangedfact. Decidebefore an issuer sends its first validator header.
Contract doc
Update the
BlobAvailableEventfield table indocs/contracts/content-fetch-issuer-contract.mdin the same change ("edit one, editboth"), including the "unknown vs. absent" semantics for
final_urlandcontent_type_raw— that distinction is the whole value of the fields and will be lost ifonly the types are documented.
Refs