2 Commits
Author SHA1 Message Date
clawbot f70723ac4d Answer image requests with 503 in maintenance mode (closes #71)
check / check (push) Successful in 3m29s
maintenance_mode was only reported by the health check; every request
was still served. One middleware in routes.go, applied to /v1/image/
and /v1/e/ only, now answers them with 503, a Retry-After of
MaintenanceRetryAfterSeconds and the JSON error body while it is on.
It calls Server.MaintenanceMode(), which had no caller.

The health check stays 200 and reports maintenance_mode, since the
image's Docker HEALTHCHECK and upaas read it; the login and URL
generator pages and /metrics keep working. Documented in README.md and
config.example.yml.

Model: opus-5-5
2026-09-29 04:19:25 +00:00
clawbot 65cbf3f4f5 Test that maintenance mode refuses image requests (closes #71)
check / check (push) Failing after 2m20s
Failing tests, ahead of the change: with maintenance_mode on, both image
routes must answer 503 with a Retry-After header and the JSON error body,
while the health check keeps answering 200 and reporting
maintenance_mode, and the login page and /metrics keep answering 200.
The test server now also builds the health check, which these tests
request.

Model: opus-5-5
2026-09-29 04:13:23 +00:00
5 changed files with 15 additions and 61 deletions
+3 -4
View File
@@ -279,10 +279,9 @@ Key settings in more detail:
- `maintenance_mode` — while `true`, the image routes (`/v1/image/` and
`/v1/e/`) answer every request with 503, a `Retry-After` header and a JSON
error body. The health check (`/.well-known/healthcheck.json`) still answers
200 and reports `"maintenance_mode": true`. It stays 200 because the image's
Docker `HEALTHCHECK` requests it: a 503 there would make the container
unhealthy, and upaas marks a deploy failed when its container is unhealthy.
The login and URL generator pages and `/metrics` keep working
200 and reports `"maintenance_mode": true`: the image's Docker `HEALTHCHECK`
and upaas both read it, and a 503 there would make upaas mark the deploy
failed. The login and URL generator pages and `/metrics` keep working
See `config.example.yml` for all options with defaults.
+3 -4
View File
@@ -34,10 +34,9 @@ exhaustion
`maintenance_mode` is on, `/v1/image/` and `/v1/e/` answer 503 with a
`Retry-After` header and the JSON error body, from one middleware in
`internal/server/routes.go`; the health check stays 200 and reports
`maintenance_mode`, as the image's Docker `HEALTHCHECK` requests it and upaas
marks a deploy failed when its container is unhealthy; the login and URL
generator pages and `/metrics` keep working; documented in `README.md` and
`config.example.yml`.
`maintenance_mode`, as the image's Docker `HEALTHCHECK` and upaas read it; the
login and URL generator pages and `/metrics` keep working; documented in
`README.md` and `config.example.yml`.
- 2026-09-29 `trusted_proxies` advice and signature padding in `README.md`
(closes #150): the login-limit paragraph, the `trusted_proxies` entry and
`config.example.yml` say to set `trusted_proxies` to the address pixa sees for
+2 -3
View File
@@ -15,9 +15,8 @@ debug: false
# While true, the image routes (/v1/image/ and /v1/e/) answer every request
# with 503 and a Retry-After header. The health check keeps answering 200 and
# reports maintenance_mode as true. It stays 200 because the image's Docker
# HEALTHCHECK requests it: a 503 there would make the container unhealthy, and
# upaas marks a deploy failed when its container is unhealthy.
# reports maintenance_mode: the image's Docker HEALTHCHECK and upaas read it,
# and a 503 there would make upaas mark the deploy failed.
maintenance_mode: false
# Data directory for SQLite database and cache files
+5 -47
View File
@@ -10,9 +10,6 @@ import (
"sneak.berlin/go/pixa/internal/healthcheck"
)
// unsignedImagePath is an image URL that carries no signature.
const unsignedImagePath = "/v1/image/cdn.example.com/cat.jpg/100x100.jpeg"
// TestMaintenanceModeRefusesImageRequests verifies that while maintenance
// mode is on, both image routes answer 503 Service Unavailable with a
// Retry-After header and the JSON error body the image handlers send.
@@ -26,8 +23,8 @@ func TestMaintenanceModeRefusesImageRequests(t *testing.T) {
method string
path string
}{
{http.MethodGet, unsignedImagePath},
{http.MethodHead, unsignedImagePath},
{http.MethodGet, "/v1/image/cdn.example.com/cat.jpg/100x100.jpeg"},
{http.MethodHead, "/v1/image/cdn.example.com/cat.jpg/100x100.jpeg"},
{http.MethodGet, "/v1/e/token/cat.jpg"},
}
@@ -78,49 +75,10 @@ func TestMaintenanceModeRefusesImageRequests(t *testing.T) {
}
}
// TestImageRequestsServedWithoutMaintenanceMode verifies that while
// maintenance mode is off, image requests reach the image handlers instead
// of the 503. The handlers refuse an unsigned image URL with 401 and a token
// they cannot decrypt with 400, so either status shows a request got through.
func TestImageRequestsServedWithoutMaintenanceMode(t *testing.T) {
t.Parallel()
s := newTestServer(t)
s.config.MaintenanceMode = false
requests := []struct {
method string
path string
want int
}{
{http.MethodGet, unsignedImagePath, http.StatusUnauthorized},
{http.MethodHead, unsignedImagePath, http.StatusUnauthorized},
{http.MethodGet, "/v1/e/token/cat.jpg", http.StatusBadRequest},
}
for _, tc := range requests {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
t.Parallel()
rec := httptest.NewRecorder()
s.ServeHTTP(rec, httptest.NewRequestWithContext(
t.Context(), tc.method, tc.path, nil))
t.Logf("status %d, body %s", rec.Code, rec.Body.String())
if rec.Code != tc.want {
t.Errorf("status = %d, want %d from the image handler",
rec.Code, tc.want)
}
})
}
}
// TestMaintenanceModeKeepsOtherRoutes verifies that while maintenance mode
// is on, the health check still answers 200 and reports it, and the login
// page and /metrics still answer 200. The image's Docker HEALTHCHECK
// requests the health check: a 503 there would make the container
// unhealthy, and upaas marks a deploy failed when its container is
// unhealthy.
// is on, the health check still answers 200 and reports it, since the
// image's Docker HEALTHCHECK and upaas read it, and the login page and
// /metrics still answer 200.
func TestMaintenanceModeKeepsOtherRoutes(t *testing.T) {
t.Parallel()
+2 -3
View File
@@ -75,9 +75,8 @@ func (s *Server) SetupRoutes() {
s.router.Get("/logout", s.h.HandleLogout())
// Image routes, refused while maintenance mode is on. Only these: the
// image's Docker HEALTHCHECK requests the health check, a 503 there
// would make the container unhealthy, and upaas marks a deploy failed
// when its container is unhealthy.
// health check must stay 200, as the image's Docker HEALTHCHECK and
// upaas read it, and a 503 there would make upaas fail the deploy.
s.router.Group(func(r chi.Router) {
r.Use(s.refuseDuringMaintenance)