Let browsers cache GetImage responses

Merged
BO
21d ago
+143-18

What does this PR aim to accomplish?:

Images served by GetImage (e.g. https://assets.xmoj-script.uk/GetImage?ImageID=…) load slowly on every page view: the response has no Cache-Control, ETag or Last-Modified, so browsers can't cache it, and each view goes Worker → GitHub API again (measured ~0.7–2s per view; a conditional request still returned the full 200).

It also fixes a latent bug: when GitHub fails (404, rate limit), GetImage returned 200 image/png with GitHub's error JSON as the body.

How does this PR accomplish the above?:

Image IDs are 32 random letters and never reused, so a found image is safe to cache forever.

  • Found images: Cache-Control: public, max-age=31536000, immutable and ETag: "<ImageID>"; still content-type: image/png.
  • If-None-Match matching the ID returns 304 without calling GitHub.
  • The GitHub body is streamed through instead of buffered into a Blob.
  • GitHub errors keep GitHub's status and are no-store, and an unreachable GitHub returns 502 with no-store, so a transient failure is never pinned in browser caches.
  • Dropped the ?1=1 query on the GitHub API URL: authenticated API responses aren't cached by the Worker's fetch, so it had no effect.

Tests: 4 new cases in test/process.test.js (immutable + bytes, 304 without fetch, error passthrough uncached, 502 uncached), all through Process(). npm test: 152/152 pass; wrangler deploy --dry-run builds.

Does this PR need a documentation update?:

No.


By submitting this pull request, I confirm the following:

  1. I have read and understood the contributor's guide, as well as this entire template. I understand which branch to base my commits and Pull Requests against. (dev for XMOJ-Script, master for everything else).
  2. I have commented on my proposed changes within the code.
  3. I have tested my changes.
  4. I am willing to help maintain this change if there are issues with it later.
  5. It is compatible with the GNU General Public License v3.0
  6. I have squashed any insignificant commits. (git rebase)
  7. I have checked that another pull request for this purpose does not exist.
  8. I have considered and confirmed that this submission will be valuable to others.
  9. I accept that this submission may not be used, and the pull request can be closed at the will of the maintainer.
  10. I give this submission freely and claim no ownership to its content.
  11. I have verified that my changes work correctly in all four UI combinations:
    • the script's monochrome UI on XMOJ's new UI (/web pages)
    • the script's monochrome UI on XMOJ's old/classic pages
    • the script's classic UI on XMOJ's new UI (/web pages)
    • the script's classic UI on XMOJ's old/classic pages

  • I have read the above and my PR is ready for review. Check this box to confirm

šŸ¤– Generated with Claude Code

Summary by Sourcery

Improve GetImage caching, validation, and error handling so image delivery is faster and failures are not incorrectly cached.

New Features:

  • Enable long-term browser caching and conditional requests for immutable uploaded images served by GetImage.
  • Support serving the project logo through GetImage with replaceable, short-lived caching.

Bug Fixes:

  • Preserve GitHub error statuses and prevent caching of errors, including returning 502 for unreachable GitHub services.
  • Reject invalid image identifiers before accessing GitHub.

Enhancements:

  • Stream image responses directly and handle ETag validation without unnecessary GitHub requests.

Tests:

  • Add coverage for image caching, conditional responses, error handling, identifier validation, ETag matching, and logo behavior.

Summary by cubic

Lets browsers cache GetImage responses so images no longer take roughly 1–2 seconds to load on every page view.

Found images are now marked cache-control: public, max-age=31536000, immutable with an ID-based ETag; a matching If-None-Match returns 304 without calling GitHub, and the image body is streamed instead of buffered into a Blob. Matching uses weak comparison over a tag list per RFC 9110, so W/"id" or "a", "id" still revalidate.

Also fixes a bug where GitHub errors were returned as 200 image/png with the error JSON as the body. Error responses now keep GitHub's status, and an unreachable GitHub returns 502; both are marked no-store so a transient failure is never pinned in browser caches. Image IDs that aren't 32 lowercase letters (the only format UploadImage issues) are rejected with 400 before reaching GitHub, where ../ segments could resolve to other repo paths. The project logo remains reachable via logo / logo.png, cached for a day without an ID-based ETag so it can be replaced in place.