feat(api): add etag/304 short-circuit caching to /api/streams/:id - #1340
Merged
greatest0fallt1me merged 4 commits intoJul 29, 2026
Merged
Conversation
* 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
|
@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! 🚀 |
Contributor
|
LGTM ✅ green CI, clean work — merging! |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 withIf-None-Matchand getting a304 Not Modifiedwith no body.What changed
Core
app/api/streams/[id]/route.ts— GET now emits a strong, tenant-scoped SHA-256ETag,Cache-Control: private, max-age=0, must-revalidate, and honorsIf-None-Matchwith 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, andifNoneMatchMatches(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 forcanonicalize,computeETag(incl. tenant isolation and null/undefined determinism), and all fourIf-None-Matchvariants + malformed.app/api/streams/[id]/route.test.ts— extended with a newdescribeblock 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-Matchrules, mutation invalidation behavior, a worked example session, and security considerations.README.md— docs index updated.Wire format
Security
idcannot share a tag and a malicious proxy/cache cannot poison tenant B from tenant A.Cache-Control: privateforbids shared caches (CDNs, corporate proxies) from reusing the response.If-None-Matchsimply misses instead of erroring (request falls through to a normal 200).Acceptance criteria
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 runnpm test -- --testPathPattern="(lib/etag|api/streams/\[id\]/route)"locally before merge to confirm (this sandbox'sjestrunner crashes withBus errordue to environmental constraints, not a code issue).Closes #574