Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,10 @@ Guideline:

- Ensure pages provide the configured JaWS resources and Request key metadata;
`HeadHTML` is the usual way to emit them.
- Include the `no-store` Cache-Control directive on every page response containing
`HeadHTML` or equivalent Request-key metadata; the key is a one-use capability.
`ui.Handler` sets this automatically, while custom page handlers must set it
explicitly.
- `TailHTML` is optional; it applies queued attr/class updates before the
WebSocket connects and can reduce initial flicker.
- Register the JaWS `/jaws/` route prefix correctly and pair request creation with `UseRequest` handling.
Expand Down
7 changes: 7 additions & 0 deletions AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ The normal page flow has two related HTTP requests:
key, claims the pending Request through `UseRequest`, upgrades the connection,
and begins event and DOM-update processing.

Page responses containing `HeadHTML` or equivalent Request-key metadata must
include the `no-store` Cache-Control directive. An HTTP-cached copy would repeat
the consumed one-use capability and cannot establish another WebSocket
connection. `ui.Handler` sets `Cache-Control: no-store` automatically; custom
page handlers must set it explicitly. A bfcache restoration is handled
separately by the bundled client's `pageshow` reload.

Applications that emit equivalent resources and metadata need not call
`HeadHTML` or `TailHTML`. Custom routers may parse the trailing key with
`key.Parse`, call `UseRequest`, return 404 when it is absent, and then call the
Expand Down
5 changes: 4 additions & 1 deletion lib/ui/AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ the dot callback returns an attribute with the same name. When dot implements
each wrapper's initial render; equal Template values may therefore render
different Element-specific attributes without storing them in the Template.
The callback is not invoked during `Template.JawsUpdate`. Full page templates
belong in `ui.Handler`; static structural inclusion should use Go's native
belong in `ui.Handler`, which marks every response `Cache-Control: no-store` so
an HTTP cache does not reuse a one-use Request key emitted by
`Request.HeadHTML`. Custom page handlers that emit `Request.HeadHTML` must
include the same directive. Static structural inclusion should use Go's native
template action:

```gotemplate
Expand Down
6 changes: 5 additions & 1 deletion lib/ui/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func (sr *statusRecorder) WriteHeader(code int) {
}

func (h uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
rq := h.NewRequest(r)
sr := &statusRecorder{ResponseWriter: w}
rw := RequestWriter{Request: rq, Writer: sr}
Expand Down Expand Up @@ -120,7 +121,10 @@ func (h uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// lookupers and rendered with a [With] value as the template data, exposing
// dot through its Dot field. Unless the response already has a Content-Type,
// Handler sets it to "text/html; charset=utf-8" when rendering writes its first
// bytes. A render failure before any output retains [http.Error]'s text response.
// bytes. Handler always sets Cache-Control to "no-store", replacing any value
// already present, so an HTTP cache does not reuse responses containing the
// one-use request key emitted by [jaws.Request.HeadHTML]. A render failure before
// any output retains [http.Error]'s text response.
//
// Handler renders without a generated wrapper and does not use dot as a tag, so
// dot may be arbitrary template data. The handler reuses dot across requests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ func TestHandler_PreservesExplicitContentType(t *testing.T) {
}
}

func TestHandler_SetsCacheControlNoStore(t *testing.T) {
jw, err := jaws.New()
if err != nil {
t.Fatal(err)
}
t.Cleanup(jw.Close)
if err = jw.AddTemplateLookuper(template.Must(template.New("page").Parse("<main>hello</main>"))); err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
rr.Header().Set("Cache-Control", "public, max-age=3600")
Handler(jw, "page", nil).ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))

if got, want := rr.Header().Get("Cache-Control"), "no-store"; got != want {
t.Fatalf("Cache-Control = %q, want %q", got, want)
}
}

func TestHandler_RenderFailureBeforeOutputUsesTextContentType(t *testing.T) {
jw, err := jaws.New()
if err != nil {
Expand All @@ -79,6 +98,9 @@ func TestHandler_RenderFailureBeforeOutputUsesTextContentType(t *testing.T) {
if got, want := rr.Header().Get("Content-Type"), "text/plain; charset=utf-8"; got != want {
t.Fatalf("Content-Type = %q, want %q", got, want)
}
if got, want := rr.Header().Get("Cache-Control"), "no-store"; got != want {
t.Fatalf("Cache-Control = %q, want %q", got, want)
}
}

func TestHandler_EmptyWriteBeforeErrorReturns500(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ func (rq *Request) releaseBuffersLocked() (buffers *requestBuffers) {
}

// HeadHTML writes the configured resources and Request key metadata for the page head.
//
// An HTTP response containing this output must include the "no-store"
// Cache-Control directive. [github.com/linkdata/jaws/lib/ui.Handler] sets it
// automatically. The metadata includes a one-use Request key; a copy replayed
// from an HTTP cache cannot establish another WebSocket connection.
func (rq *Request) HeadHTML(w io.Writer) (err error) {
rq.mu.RLock()
jawsKey := rq.JawsKey
Expand Down
Loading