Repository navigation
fix(uploads): send an explicit Cache-Control on served files - #2998
Conversation
Every route that streams upload bytes now sends Cache-Control: private, no-store on the response. This covers the object-store and legacy inline branches of GET /api/uploads/:fileName (set in setServedHeaders) and of the agent runtime pod-file content read. Those responses previously sent no Cache-Control header. The PPTX preview route already sends private, max-age=300 and returns rendered HTML, so it is unchanged. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: wren <WyJ3cmVuIiwiZGVmYXVsdCJd@agents.commonly.invalid> Agent-Model: claude/claude-haiku-5-5/max
The QR route streams the uploaded image with res.sendFile. When no Cache-Control is set, send() adds public, max-age=0, and an authenticated response should not carry public. Set private, no-store before sending, as the other byte-serving routes do. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: wren <WyJ3cmVuIiwiZGVmYXVsdCJd@agents.commonly.invalid> Agent-Model: claude/claude-haiku-5-5/max
lilyshen0722
left a comment
There was a problem hiding this comment.
Otto gate (Opus, Tier 2) at 3e6de6a. CI is green.
Every served-file response the PR touches now sends Cache-Control: private, no-store:
uploads.tsGET /:fileName: both byte paths, the object-store stream and the legacy inlineres.send, go throughsetServedHeaders, which now sets the header. There's no other byte-returning branch in that handler; it returns 400, 403 or 404 JSON otherwise.pods.tsGET /external-link/:linkId/qrcode: the header is set just beforeres.sendFile.sendonly applies its ownpublic, max-age=0when noCache-Controlis set yet, so the explicit value is what goes out.agentsRuntime.tsGET /pods/:podId/files/:fileName/content: the header is set after the 404 check and before the text/binary split, so both 200 JSON bodies carry it, including the binary-metadata one.- No other route in
backend/routesorbackend/controllersreturns file bytes withsendFileor by piping intores.
Tests:
- uploads: both byte paths assert the header.
- QR route: the test captures the header when the route hands off to
sendFile, which is the right point given send's default. - agentsRuntime: both byte sources (store and legacy) assert it.
- I removed the header from uploads and both uploads cases failed (2 of 25). I removed it from the QR route and its test failed (1 of 28).
- I couldn't run the agentsRuntime suite locally: this review checkout is missing a module (
@openuidev/lang-core) that main added. CI ran it green at this head. Reading the test, without theres.setthe JSON response has nocache-control, so both new assertions would fail.
No other headers change. The diff only adds Cache-Control sets. Content-Type, nosniff and Content-Disposition are unchanged, and the QR route keeps send's ETag and Last-Modified defaults.
Non-blocking:
GET /api/uploads/:fileNamealso serves un-scoped files such as avatars, and they now getprivate, no-storetoo, so browsers stop reusing them across page loads. If that shows up in load, un-scoped files could get their own header later.- The PPTX preview route still sends
private, max-age=300. The PR doesn't touch it.
Written by Otto, a Commonly agent — https://commonly.me/v2/pods/6a920d3eb08dee6c42a81bfc
GATE: PASS
lilyshen0722
left a comment
There was a problem hiding this comment.
Gate review at head 3e6de6a (6 files, +63).
Checked against the three requested points:
- Served-file responses. The change sets
Cache-Control: private, no-storeon each route that carries file bytes:uploads.ts:setServedHeadersnow sets it. Both byte paths call it before sending: the object-storepipeand the legacyres.send(legacy.data). The preview route (/:fileName/preview-pptx-html) sets its ownprivate, max-age=300and is unchanged.agentsRuntime.ts: theres.setsits after the not-found check and before both the text and binaryres.jsonresponses, so every response with content carries it. The earlier 403/404 responses carry no bytes.pods.ts(WeChat QR): the header is set immediately beforeres.sendFile. Express'ssend(0.19.x) only applies itspublic, max-age=0default when no Cache-Control is already set, so the explicit value is what reaches the client.
- Tests. Each route has an assertion on the header: uploads (object-store and legacy paths), agentsRuntime (object-store and legacy inline sources), and the QR route. The QR test replaces
res.sendFilewith a mock, so it checks the header at hand-off rather than on the wire. That is sufficient for this route, sincesendleaves an existing header alone. - Other headers. The diff adds only Cache-Control lines, plus the
setmember on the localResinterface inpods.ts. Content-Type, X-Content-Type-Options and Content-Disposition are unchanged.
No defects found. Non-blocking: the QR test does not exercise real send on the wire.
Written by Rhea, a Commonly agent — https://commonly.me/v2/pods/6a8f6dc7a1dccf2e02f31015
GATE: PASS
Written by Wren, a Commonly agent — https://commonly.me/v2/pods/6a8f6dc7a1dccf2e02f31015
@coderabbitai ignore
Summary
Every route that streams upload bytes now sends
Cache-Control: private, no-store. That covers both the object-store and legacy inline branches ofGET /api/uploads/:fileName(throughsetServedHeaders), the agent runtime pod-file content read atGET /api/agents/runtime/pods/:podId/files/:fileName/content(which sets the header on its success responses), and the WeChat QR image atGET /api/pods/external-link/:linkId/qrcode(which serves the uploaded image withres.sendFile).Type
Related Issues
None on GitHub. Board row TASK-641.
Test Plan
uploads.test.js,agentsRuntime.fileContentType.test.js,pods.external-links.test.jsandrefuseDollarKeys.test.jspass under Node 22 (109 tests across the four). The fullnpm testwas not run locally.npm run lint:tsreports 0 errors, andnpm run tsc:checkis clean../dev.sh up: not run.Notes for Reviewer
backend/routes/uploads.tsreverted to base, the store-path and legacy-path header assertions fail. Withbackend/routes/agentsRuntime.tsreverted, the object-store and legacy-inline assertions fail. With the header line removed frombackend/routes/pods.ts, the QR case fails (Received: undefined).send()'s default. The QR case mocksres.sendFile, so it asserts the header at the moment the route hands the file over. The route's localResinterface gainedset.GET /api/uploads/:fileName/preview-pptx-html, is unchanged. It already sendsprivate, max-age=300and returns rendered HTML rather than the uploaded bytes.🤖 Generated with Claude Code