Skip to content

feat(spec,middleware): conditional requests and content coding on Layer 1 - #101

Open
ucekmez wants to merge 1 commit into
feat/cloudevents-standard-attributesfrom
feat/layer1-conditional-requests
Open

feat(spec,middleware): conditional requests and content coding on Layer 1#101
ucekmez wants to merge 1 commit into
feat/cloudevents-standard-attributesfrom
feat/layer1-conditional-requests

Conversation

@ucekmez

@ucekmez ucekmez commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 9 of a stacked series. Base is #100. Not for merge without review.

$ grep -ric 'etag|if-none-match|cache-control|304' docs/current/SPECIFICATION.md
0
$ grep -ric 'accept-encoding|content-encoding' docs/current/SPECIFICATION.md
0

Layer 1 is the polled surface of EEP — manifest, entity resolution, /eep/gates, /eep/services, capabilities — and none of it was conditionally retrievable. An agent tracking a thousand entities re-downloaded every document on every poll, in a protocol whose front-page claim is byte and token efficiency.

Manifests are not small: eep-manifest.json constrains 24 properties including nested x402, compliance, data_residency and discovery_hints objects.

docs/guides/iot-discovery.md already tells agents that "manifests are cached per their Cache-Control header"a header nothing required anyone to send.

What changed

Spec — §3.2.1 Conditional requests and caching

  • ETag REQUIRED on Layer 1 GETs; If-None-Match honoured with 304; Last-Modified/If-Modified-Since SHOULD; Cache-Control REQUIRED.
  • A publisher that cannot guarantee byte-stability MUST use a weak validator rather than a strong one it cannot honour.
  • Gated resources are private at minimum, and gates are evaluated before returning 304 — otherwise a subscriber whose access was revoked keeps revalidating a cached copy forever.

Spec — §3.2.2 Content coding

  • Honour Accept-Encoding on Layer 1 (MUST) and SSE (SHOULD).
  • A publisher compressing an event stream MUST flush at every event boundary — otherwise events sit in the compressor's buffer and the stream stops being a stream — and MUST serve uncompressed rather than break delivery latency if it cannot.

Middleware — conditional.ts

  • Strong ETag over a canonical (sorted-key) serialisation. Canonicalisation is the load-bearing part: a publisher building its manifest from a map would otherwise emit a fresh ETag per request, so no conditional request would ever hit — worse than not implementing them, because the client pays for the validator round-trip and still receives a body. There's a test pinning exactly this.
  • If-None-Match uses RFC 9110 §8.8.3.2's weak comparison, so W/"x" matches "x". A string equality check would be wrong.
  • Applied to manifest, entity, gates and services, with per-resource Cache-Control (private for gate config).
  • Only 2xx responses are made conditional — collapsing an error to 304 would tell a client its cached success is still valid.

compliance-cli — probes for ETag presence, 304 on revalidation, ETag stability across requests, Cache-Control, and gzip negotiation.

Scope

  • Spec / schema only
  • TypeScript package(s)
  • Python package(s)
  • Tests / CI
  • Docs / examples

Checklist

  • I read CONTRIBUTING.md and CODE_OF_CONDUCT.md.
  • Tests added or updated where appropriate.
  • Breaking change? New normative MUSTs, so publishers that ignore them will newly fail the Standard-tier probes added here. No schema change; no wire-format change. Existing clients that ignore ETag/Cache-Control are unaffected.
  • Documentation updated for user-visible behavior.

Verification

Suite Result
@eep-dev/middleware 158 passed (was 126)
@eep-dev/compliance-cli 73 passed
tests/ 184 passed
codegen-schema-types --check no drift

Notes for reviewers

A correction to my own earlier claim. In the audit I reported CloudEvents binary content mode as the single largest wire optimisation at "62% smaller frames". That number was computed by dropping source, time, specversion and datacontenttype rather than relocating them — it is not a like-for-like comparison. Re-derived honestly against the spec's own §4.2 example:

bytes vs structured
SSE structured (today) 334
SSE binary, lossless 266 20% smaller
SSE binary, as I originally measured 127 drops four attributes
Webhook structured 295
Webhook binary (HTTP/1.1) 254 14% smaller

Binary mode is still worth doing — it removes a JSON parse from the subscriber's hot path and its stable ce-* headers compress well under HTTP/2 HPACK across repeated deliveries — but it is a 14–20% win, not 62%, and it is not the top wire optimisation. Conditional requests are: a 304 removes the entire body rather than shrinking it. That is why this PR comes first. Binary mode follows separately with the corrected numbers.

Python middleware parity is not included here — flagging rather than silently skipping.

Compression is specified but not implemented in the middleware: it is a transport concern that Express/Fastify/Hono/Koa each solve with their own compression middleware, and the adapters return plain objects rather than owning the response stream. The spec text plus the conformance probe is what makes it checkable; forcing a compression implementation into the adapter layer would fight each framework's own.

…er 1

`grep -ri 'etag|if-none-match|cache-control|304'` across SPECIFICATION.md
returned zero matches. So did `accept-encoding`. Layer 1 is the *polled*
surface of EEP — manifest, entity resolution, gates, services,
capabilities — and none of it was conditionally retrievable. An agent
tracking a thousand entities re-downloaded every document on every poll,
in a protocol whose front-page claim is byte and token efficiency.

Manifests are not small: `eep-manifest.json` constrains 24 properties
including nested `x402`, `compliance`, `data_residency` and
`discovery_hints` objects. `docs/guides/iot-discovery.md` already told
agents that "manifests are cached per their `Cache-Control` header" — a
header nothing required anyone to send.

Spec:
- §3.2.1 Conditional requests and caching: `ETag` REQUIRED on Layer 1
  GETs, `If-None-Match` honoured with `304`, `Last-Modified` /
  `If-Modified-Since` SHOULD, `Cache-Control` REQUIRED. A publisher that
  cannot guarantee byte-stability MUST use a weak validator rather than
  a strong one it cannot honour. Gated resources are `private` at
  minimum, and gates are evaluated BEFORE returning `304` so a
  subscriber whose access was revoked cannot keep revalidating a cached
  copy.
- §3.2.2 Content coding: honour `Accept-Encoding` on Layer 1 (MUST) and
  SSE (SHOULD). A publisher compressing an event stream MUST flush at
  every event boundary — otherwise events sit in the compressor's buffer
  and the stream stops being a stream — and MUST serve uncompressed
  rather than break delivery latency if it cannot.

Middleware:
- New `conditional.ts` computing a strong ETag over a canonical
  (sorted-key) serialisation. Canonicalisation is the load-bearing part:
  a publisher building its manifest from a map would otherwise emit a
  fresh ETag per request, so no conditional request would ever hit —
  worse than not implementing them, because the client pays for the
  round-trip and still receives a body.
- `If-None-Match` uses RFC 9110's weak comparison, so `W/"x"` matches
  `"x"`. Applied to manifest, entity, gates and services, with
  `Cache-Control` chosen per resource (`private` for gate config).
- Only 2xx responses are made conditional; collapsing an error to 304
  would tell a client its cached success is still valid.

compliance-cli: probes for ETag presence, 304 on revalidation, ETag
stability across requests, Cache-Control, and gzip negotiation.

Refs: EEP audit 2026-08 findings O2, O3
Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
Copilot AI lite review requested due to automatic review settings August 26, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants