Skip to content

feat: add a development HTTP caching mode - #625

Open
joewiz wants to merge 1 commit into
masterfrom
feat/development-caching-mode
Open

feat: add a development HTTP caching mode#625
joewiz wants to merge 1 commit into
masterfrom
feat/development-caching-mode

Conversation

@joewiz

@joewiz joewiz commented Aug 19, 2026

Copy link
Copy Markdown
Member

[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 with Upload current file to localhost, and reloading to see the change. That instruction does not currently hold. Reproduced against a local instance before this change:

step result
view the page (browser caches it) Last-Modified: Mon, 29 Jun 2026 00:00:00 +0000
edit the article and upload it succeeds — the new content is in the database
reload (a conditional request, as a browser sends) 304 — the old page
force-reload 200, edit visible

The change

Setting the Java system property hsg.http.caching to off disables HTTP caching. The response then 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.

After the change, the same walkthrough returns 200 with the edit visible, on an ordinary reload.

Three decisions worth flagging for review:

  • Only the exact value off disables 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-Modified is suppressed rather than sent alongside no-store. Sending both is contradictory, and suppressing it guarantees no conditional request can be honored.
  • The x-method: head short-circuit is disabled too, for the same reason as the If-Modified-Since one.

On the name

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 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.sh honors $JAVA_OPTS:

JAVA_OPTS=-Dhsg.http.caching=off /path/to/eXist-db.app/Contents/Resources/bin/startup.sh

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:

caching on (default) caching off
sends Last-Modified pass skip
revalidates to 304 pass skip
not marked no-store pass skip
marked no-store skip pass
conditional request not answered with 304 skip pass
page still renders skip pass

The 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-Modified of its own on these responses. It is inert, since no-store prevents the response being stored at all.

This is independent of #624, which stops error responses being cached. The two touch view.xql in 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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant