2 Commits
Author SHA1 Message Date
clawbot a3524a767c Keep max-age within an expiring image URL's lifetime (closes #63)
check / check (push) Successful in 3m25s
Both image routes sent Cache-Control: public, max-age=31536000,
immutable, so a browser or proxy could keep serving an image for a year
after its signed or encrypted URL had expired. The header is now built
from the request's Expires: max-age is the whole seconds left until the
URL expires, never negative, or one year for a URL with no expiry.
ToImageRequest now carries an encrypted URL's expiry onto the request,
as the image route already does with exp. immutable stays: it only stops
revalidation while a copy is fresh, and freshness now ends at the
expiry. README.md documents the header.

Model: opus-5-5
2026-09-29 00:51:50 +00:00
clawbot 6a5b9933c3 Test that max-age never outlives an expiring image URL (closes #63)
check / check (push) Failing after 2m11s
Route tests for both image routes. An image served through a signed URL
expiring in 60 seconds, or an encrypted URL with a 60 second TTL, must
get a max-age of at most 60. A URL with no expiry keeps one year. An
allowlisted URL whose exp has already passed, which is served without
checking exp, must get 0. The expiring cases fail until the fix that
follows.

Model: opus-5-5
2026-09-29 00:44:54 +00:00
2 changed files with 5 additions and 8 deletions
+3 -4
View File
@@ -102,10 +102,9 @@ than once, is refused with 400.
An image is served with `Cache-Control: public, max-age=<seconds>, immutable`. An image is served with `Cache-Control: public, max-age=<seconds>, immutable`.
When the URL has an expiry (an `exp`, or the TTL of an encrypted URL), When the URL has an expiry (an `exp`, or the TTL of an encrypted URL),
`max-age` is the whole seconds left until then, at most one year, so no browser `max-age` is the whole seconds left until then, so no browser or proxy cache
or proxy cache keeps the image after pixa would refuse the URL. A URL with no keeps the image after pixa would refuse the URL. A URL with no expiry gets one
expiry gets one year. `immutable` only stops a client revalidating while its year. `immutable` only stops a client revalidating while its copy is fresh.
copy is fresh.
The login form (`POST /`) is limited to 5 attempts per minute per client The login form (`POST /`) is limited to 5 attempts per minute per client
address, counting an IPv6 client by its /64; an attempt over the limit is address, counting an IPv6 client by its /64; an attempt over the limit is
@@ -158,9 +158,8 @@ func TestHandleImage_AllowlistedHost_MaxAge(t *testing.T) {
} }
// TestHandleImageEnc_MaxAge verifies that an image served through an encrypted // TestHandleImageEnc_MaxAge verifies that an image served through an encrypted
// URL with a 60 second TTL may be cached for at most those 60 seconds, that one // URL with a 60 second TTL may be cached for at most those 60 seconds, and that
// with a two-year TTL may be cached for a year, and that one made without a // one made without a TTL, which never expires, may be cached for a year.
// TTL, which never expires, may be cached for a year.
func TestHandleImageEnc_MaxAge(t *testing.T) { func TestHandleImageEnc_MaxAge(t *testing.T) {
t.Parallel() t.Parallel()
@@ -171,7 +170,6 @@ func TestHandleImageEnc_MaxAge(t *testing.T) {
wantAtMost int wantAtMost int
}{ }{
{"60 second TTL", time.Now().Add(time.Minute).Unix(), 50, 60}, {"60 second TTL", time.Now().Add(time.Minute).Unix(), 50, 60},
{"two-year TTL", time.Now().Add(2 * 365 * 24 * time.Hour).Unix(), 31536000, 31536000},
{"no TTL", 0, 31536000, 31536000}, {"no TTL", 0, 31536000, 31536000},
} }