Skip to content

Emit the enriched BlobAvailableEvent fields (cannobserv#271) #10

Description

@gregoryfoster

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_atoccurred_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions