Skip to content

Let browsers cache GetImage responses - #78

Merged
boomzero merged 3 commits into
masterfrom
fix/image-cache-headers
Oct 8, 2026
Merged

boomzero merged 3 commits into
masterfrom
fix/image-cache-headers

Conversation

@boomzero

@boomzero boomzero commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

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.

Written for commit 3cd7fc9. Summary will update on new commits.

View guided diff Turn on auto-fix

Image IDs are random and never reused, but GetImage sent no cache headers,
so every page view re-fetched each image through the Worker and the GitHub
API (~1-2s). Found images are now immutable for a year with an ID-based
ETag (If-None-Match gets a 304 without calling GitHub), and the body is
streamed instead of buffered into a Blob.

GitHub errors used to come back as 200 image/png with the error JSON as
the body; they now keep GitHub's status and are marked no-store, and an
unreachable GitHub returns 502, so a transient failure is never cached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Sorry @boomzero, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T13:07:00.823038Z 001783a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates GetImage to stream GitHub image responses with immutable, ID-based browser caching and ETag revalidation, while ensuring GitHub and network failures retain appropriate error statuses and are never cached; tests cover all new response paths.

Sequence diagram for cached GetImage responses

sequenceDiagram
    participant Browser
    participant Worker
    participant GitHub

    Browser->>Worker: GET GetImage?ImageID
    alt If-None-Match matches ImageID
        Worker-->>Browser: 304 with immutable cache headers
    else Cache miss
        Worker->>GitHub: GET image content
        GitHub-->>Worker: 200 image stream
        Worker-->>Browser: 200 streamed image with ETag
    end
Loading

Sequence diagram for uncached GetImage failures

sequenceDiagram
    participant Browser
    participant Worker
    participant GitHub

    Browser->>Worker: GET GetImage?ImageID
    Worker->>GitHub: GET image content
    alt GitHub returns an error
        GitHub-->>Worker: Error status and JSON body
        Worker-->>Browser: Same error status with no-store
    else GitHub is unreachable
        GitHub-->>Worker: Network failure
        Worker-->>Browser: 502 with no-store
    end
Loading

File-Level Changes

Change Details Files
Adds long-lived browser caching and conditional requests for immutable images while preserving streaming delivery.
  • Returns cache headers, content type, and an ID-derived ETag for successful images.
  • Short-circuits matching If-None-Match requests with 304 without contacting GitHub.
  • Streams the GitHub response body instead of buffering it as a Blob.
  • Returns the upstream image response directly from the GETImage route.
Source/Process.ts
Makes GitHub failures propagate as cache-safe HTTP errors instead of successful image responses.
  • Passes through GitHub error statuses and JSON bodies with no-store.
  • Maps unreachable GitHub requests to 502 with no-store.
  • Removes the unused query parameter from the GitHub contents URL.
Source/Process.ts
Adds end-to-end Process tests covering successful caching, revalidation, and failure behavior.
  • Verifies immutable headers and response bytes.
  • Verifies 304 responses avoid the GitHub fetch.
  • Verifies 404 passthrough and uncached 502 network failures.
test/process.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@boomzero

boomzero commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

I should add this is why our service feels slower than @langningchen 's https://image.langningchen.com/

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread Source/Process.ts Outdated
Comment thread Source/Process.ts
Comment thread Source/Process.ts
Reject any ImageID that is not 32 lowercase letters (the only format
UploadImage issues; every stored image matches) with 400 before building
the PAT-authenticated GitHub URL, where "../" segments resolved to other
paths in the repo. Match If-None-Match with weak comparison over a tag
list, per RFC 9110, so W/"id" or "a", "id" still get a 304.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pull-request-size pull-request-size Bot added size/L and removed size/M labels Oct 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Requires human review: Adds browser caching (1-year immutable, ETag/304) to GetImage, fixes GitHub error and 502 status passthrough, and validates ImageIDs, with tests. Needs human sign-off on the 1-year immutable cache tradeoff for the public endpoint.

View guided diff | Turn on auto-fix | Re-trigger cubic

Admins embed the project logo in discussion-board posts as
GetImage?ImageID=logo / logo.png, which the 32-letter ID check rejected.
Allow those two IDs, but cache them for a day without the ID-based ETag:
unlike random upload IDs, the logo can be replaced in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boomzero
boomzero merged commit 5b894ba into master Oct 8, 2026
7 checks passed
@boomzero
boomzero deleted the fix/image-cache-headers branch October 8, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant