feat: add a development HTTP caching mode - #625
Open
joewiz wants to merge 1 commit into
Open
Conversation
hsg-shell describes a page's freshness by $config:EDITORIAL_DATE_TIME, the editorial state of the site, rather than by the mtime of any one document. That is deliberate — raising the constant re-generates every page still being served from a cache — and it is right in production, where content arrives by deployment. It is awkward during local preview. Uploading a document does not move the editorial date, so a client holding a cached copy is told nothing changed and keeps showing the previous rendering. Our setup directions ask a new user to edit an article, upload it and reload; that instruction does not currently hold, because the reload returns 304 and the edit appears only after a force-reload. Setting the Java system property hsg.http.caching to "off" now disables HTTP caching: the response carries Cache-Control: no-store, sends no Last-Modified, and takes no part in conditional-request handling. Neither short-circuit in view.xql fires, since both exist only to serve a cache revalidating what it already holds. The property and $config:HTTP_CACHING_ENABLED are named for HTTP deliberately. They have nothing to do with the server-side caches built on eXist's cache: module — "hsg-search" in search.xqm, and "last-modified" in sitemap-config.xqm, whose name is especially easy to confuse with the header this governs. Those cache data inside the database; this governs what we tell a client. Only the exact value "off" disables it. An unset property, or a typo, leaves HTTP caching enabled, so production cannot lose it by accident and needs no configuration to keep behaving as it does today. eXist still emits a Last-Modified of its own on these responses. It is inert, since no-store prevents the response being stored at all. Adds tests/bats/caching-mode.bats, whose two suites are mutually exclusive and each skip when the instance was not started for them, so the file is meaningful either way. The whole suite also skips when no publication data is installed, since hsg-shell cannot render a page without it and every route answers 400 — the case in the GitHub workflow, which installs only hsg-shell and its libraries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
[This PR was prompted by Joe, drafted by Claude Code, and reviewed by Joe.]
The problem
hsg-shell describes a page's freshness by
$config:EDITORIAL_DATE_TIME— the editorial state of the site — rather than by the mtime of any one document. That is deliberate, and right in production, where content arrives by deployment and raising the constant re-generates every page still being served from a cache.It is awkward during local preview. Uploading a document does not move the editorial date, so a client holding a cached copy is told nothing changed and keeps showing the previous rendering.
This is not hypothetical: our setup directions walk a new user through editing
rdcr/articles/afghanistan.xml, uploading it withUpload current file to localhost, and reloading to see the change. That instruction does not currently hold. Reproduced against a local instance before this change:Last-Modified: Mon, 29 Jun 2026 00:00:00 +0000The change
Setting the Java system property
hsg.http.cachingtooffdisables HTTP caching. The response then carriesCache-Control: no-store, sends noLast-Modified, and takes no part in conditional-request handling — neither short-circuit inview.xqlfires, since both exist only to serve a cache revalidating what it already holds.After the change, the same walkthrough returns 200 with the edit visible, on an ordinary reload.
Three decisions worth flagging for review:
offdisables caching. An unset property, or a typo, leaves caching enabled. Production needs no configuration to keep behaving exactly as it does today, and cannot lose caching by accident.Last-Modifiedis suppressed rather than sent alongsideno-store. Sending both is contradictory, and suppressing it guarantees no conditional request can be honored.x-method: headshort-circuit is disabled too, for the same reason as theIf-Modified-Sinceone.On the name
The property and
$config:HTTP_CACHING_ENABLEDare named for HTTP deliberately. They have nothing to do with the server-side caches built on eXist'scache:module —"hsg-search"insearch.xqm, and"last-modified"insitemap-config.xqm, whose name is especially easy to mistake for the header this governs. Those cache data inside the database; this governs what we tell a client about the freshness of a response.How it is set
startup.shhonors$JAVA_OPTS:A container can pass it through
JAVA_TOOL_OPTIONS, which is how the hsg-project image already supplies its JVM options. A sensible follow-up is to set it there, so a development instance is correct out of the box.Tests
tests/bats/caching-mode.bats— two mutually exclusive suites, each skipping when the instance was not started for it, so the file is meaningful either way. I verified both by restarting eXist each way:Last-Modifiedno-storeno-storeThe suite also skips entirely when the instance has no publication data installed, since hsg-shell cannot render a page without it and every route answers 400. That is the case in the GitHub workflow, which installs only hsg-shell and the libraries it depends on.
Notes for review
eXist emits a
Last-Modifiedof its own on these responses. It is inert, sinceno-storeprevents the response being stored at all.This is independent of #624, which stops error responses being cached. The two touch
view.xqlin nearby places and will conflict slightly; #624 is the smaller change and is further along, so merging it first and rebasing this one is probably easiest.