Skip to content

Static files served with Last-Modified = now() instead of the file mtime #1323

@lovasoa

Description

@lovasoa

Every static file is served with Last-Modified set to SystemTime::now() rather than the file's real modification time. The header therefore changes on every request and carries no cache-validation information. There is also no ETag or Cache-Control, and the local-filesystem and database-filesystem "modified since" comparisons disagree (> vs >=), so conditional-request behavior differs by backend.

Reproduction (in-memory SQLite, no other setup)

mkdir /tmp/sp && cd /tmp/sp
printf 'hello static world\n' > asset.txt
touch -t 202001010000 asset.txt          # file mtime is 2020-01-01
SQLPAGE_DATABASE_URL='sqlite::memory:' sqlpage &
curl -sI http://localhost:8080/asset.txt | grep -i last-modified
sleep 1
curl -sI http://localhost:8080/asset.txt | grep -i last-modified

Observed (SQLPage v0.44.1), despite the file mtime being 2020-01-01:

last-modified: Fri, 12 Jun 2026 12:02:22 GMT
last-modified: Fri, 12 Jun 2026 12:02:23 GMT

The value is "now" and advances by one second between the two requests. A client doing If-Modified-Since revalidation can never get a meaningful answer, and caches cannot key on it.

Root cause

Last-Modified is built from SystemTime::now() when serving a file: src/webserver/http.rs#L469.

The comparison contract also differs between backends: the local filesystem uses strict > (src/filesystem.rs#L254) while the database filesystem query uses >= (src/filesystem.rs#L303), so a file modified in the same second as the last check is treated differently depending on where it is stored.

Suggested fix

Set Last-Modified from the actual source mtime (the value is already read for cache validation in modified_since), or omit the header when the mtime is unknown. Pick one comparison contract (> or >=) and use it for both the local and DB filesystems. Optionally add ETag / Cache-Control for static assets while here.

Risk and mitigation

Low. Correct Last-Modified strictly improves caching and revalidation. The mtime is available for both local files (fs::metadata) and DB-backed files (the last_modified column on sqlpage_files), so no information is missing.

Split out from #1249 (item 3).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions