2 Commits
Author SHA1 Message Date
clawbot a7bb770e60 Answer image requests with 503 in maintenance mode (closes #71)
check / check (push) Successful in 2m54s
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: 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.
Documented in README.md and config.example.yml.

Model: opus-5-5
2026-09-29 05:10:06 +00:00
clawbot 89ab5378a1 Test that maintenance mode refuses image requests (closes #71)
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; these fail until the next
commit. With it off, image requests must still reach the image handlers.
The test server now also builds the health check, which these tests
request.

Model: opus-5-5
2026-09-29 05:10:06 +00:00
5 changed files with 61 additions and 15 deletions
+4 -3
View File
@@ -279,9 +279,10 @@ Key settings in more detail:
- `maintenance_mode` — while `true`, the image routes (`/v1/image/` and - `maintenance_mode` — while `true`, the image routes (`/v1/image/` and
`/v1/e/`) answer every request with 503, a `Retry-After` header and a JSON `/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 error body. The health check (`/.well-known/healthcheck.json`) still answers
200 and reports `"maintenance_mode": true`: the image's Docker `HEALTHCHECK` 200 and reports `"maintenance_mode": true`. It stays 200 because the image's
and upaas both read it, and a 503 there would make upaas mark the deploy Docker `HEALTHCHECK` requests it: a 503 there would make the container
failed. The login and URL generator pages and `/metrics` keep working unhealthy, and upaas marks a deploy failed when its container is unhealthy.
The login and URL generator pages and `/metrics` keep working
See `config.example.yml` for all options with defaults. See `config.example.yml` for all options with defaults.
+4 -3
View File
@@ -34,9 +34,10 @@ exhaustion
`maintenance_mode` is on, `/v1/image/` and `/v1/e/` answer 503 with a `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 `Retry-After` header and the JSON error body, from one middleware in
`internal/server/routes.go`; the health check stays 200 and reports `internal/server/routes.go`; the health check stays 200 and reports
`maintenance_mode`, as the image's Docker `HEALTHCHECK` and upaas read it; the `maintenance_mode`, as the image's Docker `HEALTHCHECK` requests it and upaas
login and URL generator pages and `/metrics` keep working; documented in marks a deploy failed when its container is unhealthy; the login and URL
`README.md` and `config.example.yml`. 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` - 2026-09-29 `trusted_proxies` advice and signature padding in `README.md`
(closes #150): the login-limit paragraph, the `trusted_proxies` entry and (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 `config.example.yml` say to set `trusted_proxies` to the address pixa sees for
+3 -2
View File
@@ -15,8 +15,9 @@ debug: false
# While true, the image routes (/v1/image/ and /v1/e/) answer every request # 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 # with 503 and a Retry-After header. The health check keeps answering 200 and
# reports maintenance_mode: the image's Docker HEALTHCHECK and upaas read it, # reports maintenance_mode as true. It stays 200 because the image's Docker
# and a 503 there would make upaas mark the deploy failed. # HEALTHCHECK requests it: a 503 there would make the container unhealthy, and
# upaas marks a deploy failed when its container is unhealthy.
maintenance_mode: false maintenance_mode: false
# Data directory for SQLite database and cache files # Data directory for SQLite database and cache files
+47 -5
View File
@@ -10,6 +10,9 @@ import (
"sneak.berlin/go/pixa/internal/healthcheck" "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 // TestMaintenanceModeRefusesImageRequests verifies that while maintenance
// mode is on, both image routes answer 503 Service Unavailable with a // mode is on, both image routes answer 503 Service Unavailable with a
// Retry-After header and the JSON error body the image handlers send. // Retry-After header and the JSON error body the image handlers send.
@@ -23,8 +26,8 @@ func TestMaintenanceModeRefusesImageRequests(t *testing.T) {
method string method string
path string path string
}{ }{
{http.MethodGet, "/v1/image/cdn.example.com/cat.jpg/100x100.jpeg"}, {http.MethodGet, unsignedImagePath},
{http.MethodHead, "/v1/image/cdn.example.com/cat.jpg/100x100.jpeg"}, {http.MethodHead, unsignedImagePath},
{http.MethodGet, "/v1/e/token/cat.jpg"}, {http.MethodGet, "/v1/e/token/cat.jpg"},
} }
@@ -75,10 +78,49 @@ 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 // TestMaintenanceModeKeepsOtherRoutes verifies that while maintenance mode
// is on, the health check still answers 200 and reports it, since the // is on, the health check still answers 200 and reports it, and the login
// image's Docker HEALTHCHECK and upaas read it, and the login page and // page and /metrics still answer 200. The image's Docker HEALTHCHECK
// /metrics still answer 200. // requests the health check: a 503 there would make the container
// unhealthy, and upaas marks a deploy failed when its container is
// unhealthy.
func TestMaintenanceModeKeepsOtherRoutes(t *testing.T) { func TestMaintenanceModeKeepsOtherRoutes(t *testing.T) {
t.Parallel() t.Parallel()
+3 -2
View File
@@ -75,8 +75,9 @@ func (s *Server) SetupRoutes() {
s.router.Get("/logout", s.h.HandleLogout()) s.router.Get("/logout", s.h.HandleLogout())
// Image routes, refused while maintenance mode is on. Only these: the // Image routes, refused while maintenance mode is on. Only these: the
// health check must stay 200, as the image's Docker HEALTHCHECK and // image's Docker HEALTHCHECK requests the health check, a 503 there
// upaas read it, and a 503 there would make upaas fail the deploy. // would make the container unhealthy, and upaas marks a deploy failed
// when its container is unhealthy.
s.router.Group(func(r chi.Router) { s.router.Group(func(r chi.Router) {
r.Use(s.refuseDuringMaintenance) r.Use(s.refuseDuringMaintenance)