Document and test that unrouted methods get 405 without Allow on /s/*
check / check (push) Successful in 3m19s

A method chi does not route, such as PROPFIND, is refused by the
top-level router before it reaches the /s group, so it gets 405
without an Allow header. The README row and the test's doc comment
now say so, and TestStaticServesOnlyGetAndHead checks PROPFIND.

Model: opus-5-5
This commit is contained in:
2026-09-29 08:34:01 +00:00
parent 205539cde7
commit 3a4f3625e8
2 changed files with 19 additions and 4 deletions
+1 -1
View File
@@ -2721,7 +2721,7 @@ abuse limit later; they are tracked as future work.
| ------ | --------------------------- | ----------- | | ------ | --------------------------- | ----------- |
| `GET` | `/` | Root redirect, 303 (authenticated → `/sources`, unauthenticated → `/pages/login`) | | `GET` | `/` | Root redirect, 303 (authenticated → `/sources`, unauthenticated → `/pages/login`) |
| `GET` | `/.well-known/healthcheck` | Health check (JSON: `status`, `now`, `uptimeSeconds`, `uptimeHuman`, `version`, `appname`, `maintenanceMode`) | | `GET` | `/.well-known/healthcheck` | Health check (JSON: `status`, `now`, `uptimeSeconds`, `uptimeHuman`, `version`, `appname`, `maintenanceMode`) |
| `GET`, `HEAD` | `/s/*` | Static file serving (embedded CSS, JS). `GET` and `HEAD` only — every other method is answered `405 Method Not Allowed` with `Allow: GET, HEAD`. Pinned by `TestStaticServesOnlyGetAndHead` | | `GET`, `HEAD` | `/s/*` | Static file serving (embedded CSS, JS). `GET` and `HEAD` only — `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `TRACE` and `CONNECT` are answered `405 Method Not Allowed` with `Allow: GET, HEAD`. Any other method (such as `PROPFIND`) is refused by chi before it reaches this route, and gets `405` without an `Allow` header. Pinned by `TestStaticServesOnlyGetAndHead` |
| `POST` | `/webhook/{uuid}` | Webhook receiver endpoint. `POST` only — every other method is answered `405 Method Not Allowed` with `Allow: POST`. Rate limited (see [Rate Limiting](#rate-limiting)) | | `POST` | `/webhook/{uuid}` | Webhook receiver endpoint. `POST` only — every other method is answered `405 Method Not Allowed` with `Allow: POST`. Rate limited (see [Rate Limiting](#rate-limiting)) |
#### Authentication Endpoints #### Authentication Endpoints
+18 -3
View File
@@ -397,9 +397,12 @@ func (e *testEnv) storedHash(t *testing.T, username string) string {
// --- /s static group --- // --- /s static group ---
// TestStaticServesOnlyGetAndHead pins the methods the static group // TestStaticServesOnlyGetAndHead pins the methods the static group
// answers: GET and HEAD are served the asset, and any other method // answers: GET and HEAD are served the asset, and the other methods
// is refused with 405 and an Allow header naming those two. The // chi routes (POST, PUT, DELETE and the rest) are refused with 405
// README documents this; the test is what keeps the two from // and an Allow header naming those two. A method chi does not route,
// such as PROPFIND, is refused with 405 by the top-level router
// before it reaches the static group, so it gets no Allow header.
// The README documents this; the test is what keeps the two from
// drifting. // drifting.
func TestStaticServesOnlyGetAndHead(t *testing.T) { func TestStaticServesOnlyGetAndHead(t *testing.T) {
t.Parallel() t.Parallel()
@@ -416,6 +419,7 @@ func TestStaticServesOnlyGetAndHead(t *testing.T) {
http.MethodPost, http.MethodPost,
http.MethodPut, http.MethodPut,
http.MethodDelete, http.MethodDelete,
"PROPFIND",
} { } {
t.Run(method, func(t *testing.T) { t.Run(method, func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -436,6 +440,17 @@ func TestStaticServesOnlyGetAndHead(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Empty(t, w.Body.Bytes(), assert.Empty(t, w.Body.Bytes(),
"HEAD must not carry a body") "HEAD must not carry a body")
case "PROPFIND":
assert.Equal(
t, http.StatusMethodNotAllowed, w.Code,
)
assert.Empty(t, w.Header().Get("Allow"),
"chi refuses a method it does not route "+
"before the static group runs")
assert.NotContains(
t, w.Body.String(), string(body),
"a refused method must not get the asset",
)
default: default:
assert.Equal( assert.Equal(
t, http.StatusMethodNotAllowed, w.Code, t, http.StatusMethodNotAllowed, w.Code,