Skip to content

Unchanged JSON answers under /v1 are 304 with an ETag #919

Closed

Description

@V3RON

Part of #905.

Scope

A client that sends back the ETag of its last answer gets 304 Not Modified with no body when a JSON read under /v1 has not changed. The console uses it in #916; any HTTP client can.

In short

Every GET under /v1 that answers 200 with JSON gains an ETag header. A later GET of the same route with If-None-Match: <that tag> answers 304 and no body while the answer is unchanged.

flowchart LR
  R[GET /v1/...] --> H[route handler] --> E["ETag step (new)"]
  E -->|tag matches| N[304, no body]
  E -->|else| J[200 JSON + ETag]
Loading

If it goes wrong: an event stream stalls because it is buffered to compute a tag, or a client sees an old answer.

Technical spec

Facts

# Fact Source
F1 No /v1 answer sets ETag or Cache-Control; only console files do. src/http/app.ts:280 (only Location), src/http/server.ts:122-126
F2 Hono's etag() runs after the handler, only for GET/HEAD/QUERY with res.ok, hashes the body (SHA-1 via crypto.subtle, chained per 256 KiB), sets a strong "…" tag, and answers 304 keeping only cache-control, content-location, date, etag, expires, vary. node_modules/hono/dist/middleware/etag/index.js:1-80, digest.js
F3 The app's middleware are added with app.use("*", …) in createHttpApp; the request log line reads c.res.status after next(). src/http/app.ts:172-212
F4 Streams answer text/event-stream: /v1/events/stream, /v1/leases/{id}/events, /v1/lease-requests/{id}/events, exec and install. src/http/AGENTS.md "Streams"; src/http/app.test.ts:1054, 1751, 2113
F5 Mounts (/v1/stream, agent-device) are registered with app.all after the routes. src/http/app.ts:762-766
F6 hono is ^4.13.5. package.json:75
F7 On a worker, GET /v1/workers stamps lastSeenAt with the time of each call and a per-read transitionAgeMs, so it never answers the same body twice and never gets a 304; on a gateway it changes only when a view refreshes. src/daemon/dispatcher.ts:902, 1080; src/gateway/worker-registry.ts:368
F8 The existing mount fake answers new Response("ok") (text/plain). src/http/app.test.ts:3119-3125

Modules touched

src/http/app.ts (the ETag step), src/http/app.test.ts, src/http/AGENTS.md (How a request runs), website/src/content/docs/docs/using-devices/remote-agents.mdx (one line on ETag/If-None-Match). Searches: ETag, If-None-Match, etag( in src/ and ui/src/: none today.

Contract and event changes

None: a response header only, no contract shape (searched src/contract/ for etag: none).

Other code on the same state

The response object: the request log middleware (app.ts:198) reads c.res.status after next(); with the ETag step inside it, a 304 logs as 304. The error boundary (app.onError) answers non-2xx, which gets no tag (F2). Searches: c.res in src/http/ (3 hits).

Failure modes

Call Outcome What happens Limit, counted from
ETag step JSON 200 tag set, 304 on match none
ETag step stream or mount skipped by content type and path, before the body is read none
ETag step error answer no tag (F2) none

No state is written.

Rules in play

  • Documentation rule 9, "a change to the public surface … updates the pages": remote-agents.mdx gains the line.
  • ADR 0044 §3 "ETag": complies.

Tests

Seam: createHttpApp through app.request, modelled on src/http/app.test.ts.

  • A client reads a JSON route twice, sending back the ETag: unchanged → 304, empty body, same tag; changed (a token created between reads of /v1/tokens) → 200, new tag; stale tag → 200; a 404 answer → no ETag; /v1/events/stream → no ETag, first event arrives; a mount answering 200 application/json (the fake changed to JSON, F8) → no ETag.

Done when

  • pnpm vitest run src/http/app.test.ts passes with the new test.
  • On a running daemon, a second GET /v1/tokens with If-None-Match: <tag of the first> answers 304 (reviewer may use the test instead). /v1/workers answers 304 only on a gateway (F7).

Out of scope

The console sending tags (#916). Cache-Control headers.

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  2. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  3. self-assigned this
    on Oct 23, 2026
  4. added 2 commits that reference this issue on Oct 23, 2026
    125d739
    ab4698e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions