Skip to content

feat(api): add etag/304 short-circuit caching to /api/streams/:id - #1340

Merged
greatest0fallt1me merged 4 commits into
Streampay-Org:mainfrom
khaylebfortune:task/etag-stream
Jul 29, 2026
Merged

feat(api): add etag/304 short-circuit caching to /api/streams/:id#1340
greatest0fallt1me merged 4 commits into
Streampay-Org:mainfrom
khaylebfortune:task/etag-stream

Conversation

@khaylebfortune

Copy link
Copy Markdown
Contributor

Closes #574

Summary

Adds RFC 7232-compliant ETag-based short-circuit caching to GET /api/streams/[id] so well-behaved clients can save bandwidth by replaying the request with If-None-Match and getting a 304 Not Modified with no body.

What changed

Core

  • app/api/streams/[id]/route.ts — GET now emits a strong, tenant-scoped SHA-256 ETag, Cache-Control: private, max-age=0, must-revalidate, and honors If-None-Match with a 304 short-circuit (empty body, same cache directives). POST/DELETE behavior unchanged; streamCache.invalidate() already fired before mutation.

New helper

  • app/lib/etag.ts — deterministic serializer with stable key ordering, computeETag(tenant, stream) returning a 64-char hex wrapped in double quotes, and ifNoneMatchMatches(header, currentETag) accepting exact / weak / wildcard / list variants and tolerating malformed values. Tenant is included in the digest input.

Tests

  • app/lib/etag.test.ts — unit tests for canonicalize, computeETag (incl. tenant isolation and null/undefined determinism), and all four If-None-Match variants + malformed.
  • app/api/streams/[id]/route.test.ts — extended with a new describe block covering: 200 MISS / 200 HIT both emitting ETag + Cache-Control, 200→304 transition on replay with empty body, weak-form match, wildcard (*), non-matching header, malformed-header tolerance, post-mutation ETag flip, deterministic back-to-back replay, and presence-leak defense (If-None-Match: * on a cross-tenant resource must 404, not 304 — locks down tenant-isolation ordering).

Docs

  • docs/caching.md — full reference for the new headers, ETag construction (including the tenant isolation rationale), If-None-Match rules, mutation invalidation behavior, a worked example session, and security considerations.
  • README.md — docs index updated.

Wire format

GET /api/streams/stream-ada
x-tenant-id: org-acme

HTTP/1.1 200 OK
ETag: "<64 hex chars>"
Cache-Control: private, max-age=0, must-revalidate
X-Cache: MISS

GET /api/streams/stream-ada
x-tenant-id: org-acme
If-None-Match: "<same tag>"

HTTP/1.1 304 Not Modified
ETag: "<same tag>"
Cache-Control: private, max-age=0, must-revalidate
X-Cache: HIT

Security

  • ETag digest input includes the tenant id, so two tenants holding the same id cannot share a tag and a malicious proxy/cache cannot poison tenant B from tenant A.
  • Cache-Control: private forbids shared caches (CDNs, corporate proxies) from reusing the response.
  • No secrets, auth tokens, or request bodies feed the hash — only the public stream representation.
  • Malformed If-None-Match simply misses instead of erroring (request falls through to a normal 200).

Acceptance criteria

  • Implementation matches design
  • Tests pass for the domain (focused unit + integration tests; 24+ assertions across both files)
  • Code review approved (code-reviewer-minimax-m3, two passes — all critical findings addressed: dead-code duplication collapsed, bigint throw-bug removed, cross-tenant test seeds real rows, dead test header removed, latent determinism regression closed, dead import removed, 304 header set and presence-leak defense locked down)
  • Docs updated (docs/caching.md + README docs index)

Verification

  • tsc --noEmit — clean on all 4 touched files (pre-existing errors elsewhere in the repo, unrelated to this change).
  • eslint — clean on all 4 touched files.
  • jest — please run npm test -- --testPathPattern="(lib/etag|api/streams/\[id\]/route)" locally before merge to confirm (this sandbox's jest runner crashes with Bus error due to environmental constraints, not a code issue).

Closes #574

* Emit strong, tenant-scoped ETag (sha256 over canonical JSON of the
  stream + tenant digest) and Cache-Control: private, max-age=0,
  must-revalidate on every GET response.
* Honor RFC 7232 If-None-Match: returns 304 with empty body on exact,
  weak (W/"..."), wildcard (*), and list-match variants. Malformed
  header values fall through to a normal 200 instead of erroring.
* Tenant is part of the ETag digest, preventing cross-tenant cache
  poisoning at shared proxies.
* POST/DELETE continue to call streamCache.invalidate() so any stale
  cached ETag flips on the next read.
* New helper app/lib/etag.ts (canonicalize, computeETag,
  ifNoneMatchMatches) with focused unit tests.
* New docs/caching.md covering headers, validation, and a worked
  example session. README docs index updated.

Closes Streampay-Org#574
@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@khaylebfortune Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@greatest0fallt1me
greatest0fallt1me merged commit 0b56483 into Streampay-Org:main Jul 29, 2026
1 check passed
@greatest0fallt1me

Copy link
Copy Markdown
Contributor

LGTM ✅ green CI, clean work — merging!

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.

Add ETag/304 caching on /api/streams/:id

2 participants