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: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

- name: Staticcheck
run: |
go install honnef.co/go/tools/cmd/staticcheck@1285a6a5ec1e0ebb658f49e82b6c566a878cc3cb # v0.8.1
go install honnef.co/go/tools/cmd/staticcheck@54455593555daf6bfcf3c480faaada7de3145357 # v0.8.2
staticcheck ./...

- name: golangci-lint
Expand All @@ -54,7 +54,7 @@ jobs:

- name: Run Gosec Security Scanner
run: |
go install github.com/securego/gosec/v2/cmd/gosec@deb54465fea23d19a77f037e11e6589021f8501d # v2.29.0
go install github.com/securego/gosec/v2/cmd/gosec@5eb1d104ef85ff2c905c877eb600e8c1dee33a22 # v2.30.0
gosec ./...

- name: Test
Expand Down
1 change: 1 addition & 0 deletions doc/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Oversized inbound WebSocket messages close the connection and log the read-limit
error. Other transport errors ordinarily end the Request without a log report;
`Debug` retains their underlying details for logging. Configure `Debug` before
serving and regenerate head markup with `GenerateHeadHTML` when changing it.
Noscript probes cancel Requests without logging or incrementing `ErrorCount`.

## Authorization

Expand Down
2 changes: 1 addition & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ var ErrReplaceNotBroadcastable = errors.New("what.Replace cannot be broadcast")
// elements, or [Element.Remove] for the per-element child form.
var ErrRemoveNotBroadcastable = errors.New("what.Remove cannot be broadcast")

// ErrJavascriptDisabled is returned when the noscript probe indicates JavaScript is disabled.
// ErrJavascriptDisabled identifies a Request canceled by the noscript probe.
var ErrJavascriptDisabled = errors.New("javascript is disabled")

// ErrWebsocketOriginMissing is returned when a WebSocket request has no Origin header.
Expand Down
8 changes: 7 additions & 1 deletion request.go
Original file line number Diff line number Diff line change
Expand Up @@ -1259,14 +1259,20 @@ func (rq *Request) runWebSocket(ws *websocket.Conn, idleInterval, wsTimeout time
// and are not reported through [Jaws.Logger]. When [Jaws.Debug] is true, their
// underlying error is retained in the Request cancellation cause, which is
// passed to [Jaws.Log] instead.
//
// A noscript probe returns 204 and cancels the Request with
// [ErrJavascriptDisabled] without logging or increasing [Jaws.ErrorCount].
func (rq *Request) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if rq.startServe() {
defer rq.stopServe()
idleInterval := rq.Jaws.WebSocketPingInterval
wsTimeout := rq.Jaws.getWebSocketTimeout()
if strings.HasSuffix(r.URL.Path, "/noscript") {
w.WriteHeader(http.StatusNoContent)
rq.cancel(ErrJavascriptDisabled)
// The probe is expected; preserve its cause without reporting it.
rq.mu.Lock()
_ = rq.cancelLocked(ErrJavascriptDisabled)
rq.mu.Unlock()
return
}
var err error
Expand Down
17 changes: 12 additions & 5 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5790,16 +5790,16 @@ func TestRecycleQueueRaceDoesNotLeak(t *testing.T) {
}
}

// TestNoscriptDuringLiveRenderRecordsJavascriptDisabled guards against re-adding a
// render-timing gate that would 404 the <noscript> probe while a streamed page is
// still rendering (its initial request context still live). The probe must reach the
// Request, return 204, and record ErrJavascriptDisabled.
func TestNoscriptDuringLiveRenderRecordsJavascriptDisabled(t *testing.T) {
// TestNoscriptDuringLiveRenderCancelsWithoutLogging checks that a probe during
// initial rendering returns 204 and cancels the Request without an error report.
func TestNoscriptDuringLiveRenderCancelsWithoutLogging(t *testing.T) {
jw, err := New()
if err != nil {
t.Fatal(err)
}
defer jw.Close()
logger := &captureErrorLogger{}
jw.Logger = logger
go jw.Serve()
waitForServeLoop(t, jw)

Expand All @@ -5819,6 +5819,13 @@ func TestNoscriptDuringLiveRenderRecordsJavascriptDisabled(t *testing.T) {
if cause := context.Cause(rq.Context()); !errors.Is(cause, ErrJavascriptDisabled) {
t.Fatalf("cancellation cause = %v, want ErrJavascriptDisabled", cause)
}
if got := jw.ErrorCount(); got != 0 {
t.Fatalf("ErrorCount() = %d, want 0", got)
}
awaitTestLoggerQueue(t, jw)
if errs := logger.snapshot(); len(errs) != 0 {
t.Fatalf("logged errors = %v, want none", errs)
}
}

// TestRequestRecycledKeyNotReusedWhileReachable proves the recycle path reserves the
Expand Down
4 changes: 2 additions & 2 deletions status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func TestJaws_StatusMetricInvalidations(t *testing.T) {
}
requireStatusDirty(t, jw, StatusMetricPendingRequests)
rq.ServeHTTP(httptest.NewRecorder(), newStatusHTTPRequest("http://example.test", "/jaws/"+rq.JawsKeyString()+"/noscript"))
requireStatusDirty(t, jw, StatusMetricActiveRequests|StatusMetricErrors)
requireStatusDirty(t, jw, StatusMetricActiveRequests)
rq.ServeHTTP(httptest.NewRecorder(), newStatusHTTPRequest("http://example.test", "/jaws/"+rq.JawsKeyString()+"/noscript"))
requireStatusDirty(t, jw, 0)
})
Expand All @@ -245,7 +245,7 @@ func TestJaws_StatusMetricInvalidations(t *testing.T) {
}
requireStatusDirty(t, jw, StatusMetricPendingRequests)
rq.ServeHTTP(httptest.NewRecorder(), newStatusHTTPRequest("http://example.test", "/jaws/"+rq.JawsKeyString()+"/noscript"))
requireStatusDirty(t, jw, StatusMetricActiveRequests|StatusMetricActiveSessions|StatusMetricErrors)
requireStatusDirty(t, jw, StatusMetricActiveRequests|StatusMetricActiveSessions)
})

t.Run("rejected Origin", func(t *testing.T) {
Expand Down
Loading