diff --git a/README.md b/README.md index c2f0c9a..862a5be 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ server { } # GET requests served directly with extension fallback - try_files /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.txt /$1.md =404; + try_files /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.avif /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.txt /$1.md /$1.bin =404; } # Upload endpoint diff --git a/build/ginxsom-fcgi_static_x86_64 b/build/ginxsom-fcgi_static_x86_64 index f53b1a6..c4fc376 100755 Binary files a/build/ginxsom-fcgi_static_x86_64 and b/build/ginxsom-fcgi_static_x86_64 differ diff --git a/config/ginxsom-local.conf b/config/ginxsom-local.conf index 74610b3..0e3e050 100644 --- a/config/ginxsom-local.conf +++ b/config/ginxsom-local.conf @@ -113,7 +113,7 @@ server { return 405; } - try_files /blobs/$1.html /blobs/$1.js /blobs/$1.mjs /blobs/$1.css /blobs/$1.txt /blobs/$1.jpg /blobs/$1.jpeg /blobs/$1.png /blobs/$1.webp /blobs/$1.gif /blobs/$1.pdf /blobs/$1.mp4 /blobs/$1.mp3 /blobs/$1.md =404; + try_files /blobs/$1.html /blobs/$1.js /blobs/$1.mjs /blobs/$1.css /blobs/$1.txt /blobs/$1.jpg /blobs/$1.jpeg /blobs/$1.png /blobs/$1.webp /blobs/$1.avif /blobs/$1.gif /blobs/$1.pdf /blobs/$1.mp4 /blobs/$1.mp3 /blobs/$1.md /blobs/$1.bin =404; # Declare UTF-8 on text responses: nginx derives Content-Type from # the file extension (.txt -> text/plain) with no charset, so diff --git a/config/local-nginx.conf b/config/local-nginx.conf index 8d30c62..5565939 100644 --- a/config/local-nginx.conf +++ b/config/local-nginx.conf @@ -298,7 +298,7 @@ http { } # GET requests - serve files directly with extension fallback - try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404; + try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.avif /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md /$1.bin =404; # Declare UTF-8 on text responses: nginx derives Content-Type from # the file extension (.txt -> text/plain) with no charset, so @@ -684,7 +684,7 @@ http { } # GET requests - serve files directly with extension fallback - try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404; + try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.avif /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md /$1.bin =404; # Declare UTF-8 on text responses: nginx derives Content-Type from # the file extension (.txt -> text/plain) with no charset, so diff --git a/docs/NGINX_CONFIG_UPDATES.md b/docs/NGINX_CONFIG_UPDATES.md index 84b1931..4e6c02b 100644 --- a/docs/NGINX_CONFIG_UPDATES.md +++ b/docs/NGINX_CONFIG_UPDATES.md @@ -223,7 +223,7 @@ server { return 405; } - try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404; + try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.avif /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md /$1.bin =404; # Cache headers add_header Cache-Control "public, max-age=31536000, immutable"; diff --git a/plans/fix-avif-serving.md b/plans/fix-avif-serving.md new file mode 100644 index 0000000..573350e --- /dev/null +++ b/plans/fix-avif-serving.md @@ -0,0 +1,149 @@ +# Fix: `.avif` files download instead of displaying + +## Symptom + +Browsers download `.avif` blobs instead of rendering them inline. Other image +types (`.jpg`, `.png`, `.webp`, `.gif`) display correctly. + +## Root Cause + +There are **two independent gaps** in the AVIF path, and they compound. + +### 1. Backend MIME to extension map is missing `image/avif` + +The centralized map in [`src/bud08.c`](src/bud08.c:98) drives every place a blob +is written to disk: + +```c +#define GINXSOM_MIME_EXTENSION_ENTRIES(X) \ + X("image/jpeg", ".jpg") \ + X("image/webp", ".webp") \ + X("image/png", ".png") \ + X("image/gif", ".gif") \ + ... (no image/avif) ... +``` + +[`mime_to_extension()`](src/bud08.c:139) falls through to `".bin"` for any +unmapped type. So an AVIF upload is stored on disk as `.bin`, while the +database records `type = "image/avif"`. The stored extension and the recorded +MIME type disagree. + +This map is used by: +- [`handle_upload_request_with_validation()`](src/main.c:1866) (BUD-02 upload) +- [`handle_mirror_request()`](src/bud04.c:427) (BUD-04 mirror) +- [`handle_get_request()`](src/main.c:1195) (FastCGI GET fallback) +- [`handle_delete_request_with_validation()`](src/main.c:1654) +- [`admin_commands.c`](src/admin_commands.c:667) + +### 2. nginx `try_files` extension lists are missing `.avif` + +GET requests for blobs are served **directly by nginx from disk** (not FastCGI). +nginx resolves the file by trying a hardcoded list of extensions: + +``` +try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg + /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404; +``` + +`.avif` is not in the list, and neither is `.bin`. Consequences: + +- If the blob was stored as `.avif`, nginx never finds it (404). +- If the blob was stored as `.bin` (the current AVIF behavior), nginx + either 404s, or — in deployments using the wildcard form + `try_files /$1* =404` documented in [`README.md`](README.md:580) — finds the + `.bin` file and serves it with `default_type application/octet-stream`. + +`application/octet-stream` plus the `X-Content-Type-Options: nosniff` header +(see [`config/ginxsom-local.conf`](config/ginxsom-local.conf:20)) forces the +browser to download rather than render. This is the observed symptom. + +Note: [`mime.types`](mime.types:19) **already** contains +`image/avif avif;`, so once the file is stored with a `.avif` extension and +nginx is allowed to find it, the correct `Content-Type: image/avif` is emitted +automatically. The MIME table is not the problem. + +## Affected Files + +| File | Change | +|------|--------| +| [`src/bud08.c`](src/bud08.c:98) | Add `X("image/avif", ".avif")` to the map | +| [`config/ginxsom-local.conf`](config/ginxsom-local.conf:116) | Add `.avif` to `try_files` | +| [`config/local-nginx.conf`](config/local-nginx.conf:301) | Add `.avif` to both `try_files` blocks (HTTP + HTTPS) | +| [`remote.nginx.config`](remote.nginx.config:187) | Add `.avif` to `try_files` | +| [`README.md`](README.md:203) | Update documented `try_files` examples | +| [`docs/NGINX_CONFIG_UPDATES.md`](docs/NGINX_CONFIG_UPDATES.md:226) | Update documented `try_files` example | + +## Fix Plan + +### Step 1 - Backend: map `image/avif` to `.avif` + +In [`src/bud08.c`](src/bud08.c:98), add an entry to +`GINXSOM_MIME_EXTENSION_ENTRIES`: + +```c +X("image/avif", ".avif") \ +``` + +This automatically: +- stores new AVIF uploads as `.avif` +- adds `image/avif` to the advertised `supported_mime_types` list in the + NIP-11 root response +- makes NIP-94 canonical URLs end in `.avif` + +### Step 2 - nginx: add `.avif` to every `try_files` list + +Add `/$1.avif` (or `/blobs/$1.avif` in the local conf) to each list. Place it +next to the other image extensions for readability. + +### Step 3 - nginx: add `.bin` to `try_files` (related robustness fix) + +Blobs whose MIME type is unknown are stored as `.bin`. They are currently +unreachable through the explicit `try_files` lists. Adding `/$1.bin` lets them +be served (as `application/octet-stream`, which is correct for unknown types). +This is optional but closes the same class of bug. + +### Step 4 - Update documentation + +Update the `try_files` examples in [`README.md`](README.md:203) and +[`docs/NGINX_CONFIG_UPDATES.md`](docs/NGINX_CONFIG_UPDATES.md:226) so future +deployments include `.avif`. + +### Step 5 - Rebuild static binaries + +Rebuild via [`Dockerfile.alpine-musl`](Dockerfile.alpine-musl:1) / +[`build_static.sh`](build_static.sh) so the updated `mime_to_extension()` map is +compiled into `build/ginxsom-fcgi_static_*`. + +### Step 6 - Verify + +1. Upload an AVIF with `Content-Type: image/avif`. +2. Confirm the file on disk is `.avif` (not `.bin`). +3. Confirm the DB `type` column is `image/avif`. +4. `curl -I https:///.avif` and confirm + `Content-Type: image/avif` and no `Content-Disposition: attachment`. +5. Load the URL in a browser and confirm it renders inline. + +## Important Caveat: Existing AVIF Blobs + +Config and code changes only affect **newly uploaded** AVIF blobs. Blobs already +stored as `.bin` will not be found by the new `.avif` `try_files` entry. +Options for existing data: + +- **Re-upload** the affected AVIF blobs (simplest, content-addressed so the + hash/URL is unchanged). +- **One-off migration**: rename `.bin` to `.avif` for rows where + `blobs.type = 'image/avif'`. Requires care because the DB is authoritative + for blob existence. + +## Optional Enhancement (out of scope unless requested) + +- Add AVIF dimension parsing to [`nip94_get_dimensions()`](src/bud08.c:284) so + NIP-94 `dim` tags are emitted for AVIF (currently only PNG/JPEG/WebP). +- Add `avif` to the URL-extension detection in + [`determine_blob_content_type()`](src/bud04.c:143) for BUD-04 mirroring. + +## Why This Is Not a `mime.types` Problem + +[`mime.types`](mime.types:19) and [`config/mime.types`](config/mime.types:19) +both already map `avif` to `image/avif`. The failure is upstream: the file is +stored with the wrong extension and/or nginx is not told to look for `.avif`. diff --git a/remote.nginx.config b/remote.nginx.config index 584322f..fa1892d 100644 --- a/remote.nginx.config +++ b/remote.nginx.config @@ -184,7 +184,7 @@ server { return 405; } - try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404; + try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.avif /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md /$1.bin =404; # Declare UTF-8 on text responses. nginx derives Content-Type from the # file extension here (e.g. .txt -> text/plain) with no charset, so diff --git a/src/bud08.c b/src/bud08.c index aeb9a3a..c35d08e 100644 --- a/src/bud08.c +++ b/src/bud08.c @@ -98,6 +98,7 @@ int nip94_get_origin(char* out, size_t out_size) { #define GINXSOM_MIME_EXTENSION_ENTRIES(X) \ X("image/jpeg", ".jpg") \ X("image/webp", ".webp") \ + X("image/avif", ".avif") \ X("image/png", ".png") \ X("image/gif", ".gif") \ X("video/mp4", ".mp4") \ diff --git a/src/ginxsom.h b/src/ginxsom.h index cd17411..2d8a058 100644 --- a/src/ginxsom.h +++ b/src/ginxsom.h @@ -10,8 +10,8 @@ // Version information (auto-updated by build system) #define VERSION_MAJOR 0 #define VERSION_MINOR 2 -#define VERSION_PATCH 4 -#define VERSION "v0.2.4" +#define VERSION_PATCH 5 +#define VERSION "v0.2.5" #include #include