Automations and webhooks for the AT Protocol airglow.run

`/static/*` assets are served without their long-lived Cache-Control header #53

Closed tyb.there-hugo.dev
Closed opened by tyb.there-hugo.dev

In production, lib/runtime/web.ts mounts /static/* with serveStatic and sets Cache-Control: public, max-age=31536000, immutable in its onFound hook. With the installed hono 4.13 and @hono/node-server 2.1.5, onFound runs after the response is built, so the header never reaches the browser.

Found while fixing #9: a /static/favicons/... file comes back 200 with no Cache-Control header at all. Hashed static assets are meant to be cached for a year, so every visit fetches them again.

Steps to reproduce #

  1. pnpm build, then pnpm serve.
  2. curl -I http://localhost:<port>/static/favicons/<any file>
  3. The response is 200 with no Cache-Control header.

Expected #

Files under /static/* that are found return Cache-Control: public, max-age=31536000, immutable. A missing file still falls through to 404 with no cache header.

Acceptance criteria #

Hint #

#9 had the same problem with /fonts/*. It set the header in a middleware after next(), only on a 200, rather than in onFound. The same approach probably works here.

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreib5rilsc5zaxyrma27kbaf7efatka6sx6bivgyuwruc5d3eod24da

Working on this, attempt 1.

loop:claim run=contribute-20261023-225304-3myla7csycu22 branch=loop/3myla7csycu22 attempt=1

fix(runtime): send the immutable cache header on /static/* assets

Production served /static/* files with no Cache-Control header, because the header was set in serveStatic's onFound hook, which runs after the response is built. The header is now set in a middleware after next(), and only on a 200. This is the same approach #9 used for /fonts/*. Found hashed assets are cached for a year again, and a missing file still falls through to a 404 with no cache header.

Verification #

  • Wrote the two new mountBuiltAssets tests first and saw the found-file one fail with no Cache-Control header. With the fix they pass.
  • The tests mount public/ the same way the existing font tests do, since public/ is what the client build copies into dist/. I did not run pnpm build && pnpm serve with curl. The tests call the same mountBuiltAssets function that production mounts on ./dist.
  • pnpm run check passes. pnpm run test passes: 161 files, 2528 tests.

Acceptance criteria #

Decisions you can reverse #

  • The note on why onFound is not used now sits once above both cache-header middlewares, instead of in the fonts comment only.
  • The header is set only on a 200. A 206 range response or a 304 gets no Cache-Control, the same as /fonts/*.

Follow-up, filed as a new issue on merge if the review agrees:

test(runtime): deflake the startWeb keep-alive stop test

lib/runtime/web.test.ts > startWeb stop > "closes keep-alive connections that keep sending requests" fails intermittently with AssertionError: expected 0 to be greater than 0 on expect(closedResponses.length).toBeGreaterThan(0).

How I know: running pnpm exec vp test lib/runtime/web.test.ts -t "keep-alive connections that keep sending" ten times in a row gave 3 failures and 7 passes. The test builds a plain Hono app and does not touch mountBuiltAssets, so the flake does not come from the /static/* cache-header change. It also failed once in a full-file run.

Where to start: the test only counts responses that come back with Connection: close once stopping is set. If the request in flight when stop() begins is the last one before the server closes the socket, or if the loop's next request hits the closed socket first and errors, no Connection: close response is recorded. Either guarantee that a request is in flight when stop() runs (for example, start stop() from inside the /busy handler), or assert on the connection closing rather than on a counted response.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3myla7csycu22

loop:pushed run=contribute-20261023-225304-3myla7csycu22 sha=19c236814c6e86c36e5e5cbea5c17ab2d29605ae

Verdict: merge. The branch does what the issue asks, and only that. /static/* now gets Cache-Control: public, max-age=31536000, immutable from a middleware that runs after next() and only on a 200, which is the approach #9 used for /fonts/*. The onFound hook that dropped the header is gone, and check and test both pass.

What I checked and how:

  • Diff (lib/runtime/web.ts): the cache middleware is registered before serveStatic on /static/*, so it wraps the static handler. A missing file falls through with no header. /fonts/* still sets public, max-age=86400; only its comment moved into the shared note. Nothing else changed. No other route serves /static/*, so the 200-only condition cannot tag an unrelated response.
  • Tests (lib/runtime/web.test.ts): two new tests go through mountBuiltAssets on public/. One checks that a found favicon returns 200 with the immutable header. The other checks that a missing file returns 404 with no Cache-Control. I swapped in the base web.ts for a run, and the found-file test failed. I then put the head version back, so the tests really catch the bug. The font tests still pass unchanged.
  • Commands: pnpm run check passes. pnpm run test passes: 161 files, 2528 tests.
  • Acceptance criteria: all four are met in the code.

Follow-up: file. The follow-up is about the flaky "closes keep-alive connections that keep sending requests" test in startWeb stop, and the claim holds. I ran it 10 times on its own and it failed once on the same closedResponses.length assertion. The test builds a plain Hono app and never touches mountBuiltAssets, and this branch does not change it. So the flake is outside this issue and is not fixed here. It is worth an issue because a flaky test can turn CI red and block merges.

loop:verdict run=review-20261023-225934-3myla7csycu22 sha=19c236814c6e86c36e5e5cbea5c17ab2d29605ae verdict=merge followup=1

Merged main into the branch: 88fd045b13bf17c23c1560d5e1eb45a6d8a20411.

loop:carry from=19c236814c6e86c36e5e5cbea5c17ab2d29605ae sha=88fd045b13bf17c23c1560d5e1eb45a6d8a20411

Merged onto main as c0bc6384658485525c79e1f655819f8673ed3f3e.

loop:merged sha=c0bc6384658485525c79e1f655819f8673ed3f3e head=88fd045b13bf17c23c1560d5e1eb45a6d8a20411

Filed the follow-up as a new issue, not marked ready: test(runtime): deflake the startWeb keep-alive stop test

loop:filed issue=3mylcd5pa3w2y

Sign up or Sign in to add to the discussion.