fix(web): proxy binary downloads through the BFF (BUNYIP-64) #82

Merged
nrupard merged 2 commits from fix/bunyip-64-download-asset-bff-proxy into main 2026-06-09 20:00:20 +02:00
Owner

Problem

Clicking Download on the Downloads page saved an HTML file instead of the binary (e.g. the vervain-agent binary came down as HTML). bunyip-api builds the asset link as the root-relative API path /v1/applications/{slug}/downloads/{asset}, but that anchor is rendered in a bunyip-web page, so the browser resolved it against the bunyip-web origin, not bunyip-api. bunyip-web had no matching route, so the request hit the not_found HTML fallback, and because the anchor carried a download attribute the browser saved that 404 page under the asset's filename.

Fix

bunyip-web is the browser-facing BFF: the session cookie is scoped to its origin and the browser must never call bunyip-api directly. This adds a streaming proxy route GET /downloads/{slug}/{asset} that re-auths the session, forwards the cookie to the API's membership/entitlement-gated download endpoint, and relays the bytes with the upstream Content-Type / Content-Disposition. Api::get_stream returns the raw reqwest::Response so a large asset streams through rather than buffering in memory.

The asset anchor now targets this proxy on the web origin and drops the download attribute: success is driven by the proxy's Content-Disposition, and any non-2xx (expired session, lost entitlement, upstream outage) navigates the browser (sign-in on 401, the downloads page otherwise) rather than being saved as the file.

OCI check (per the issue)

OCI distribution is not affected. Container images are pulled by the docker client via the docker login / docker pull command blocks shown on the page, not a browser download anchor, so they never resolve against the web origin's HTML fallback. No change needed there.

Test

just check-container green: fmt + clippy (-D warnings) + workspace lib tests all pass.

#BUNYIP-64

## Problem Clicking Download on the Downloads page saved an HTML file instead of the binary (e.g. the vervain-agent binary came down as HTML). bunyip-api builds the asset link as the root-relative API path `/v1/applications/{slug}/downloads/{asset}`, but that anchor is rendered in a bunyip-web page, so the browser resolved it against the bunyip-web origin, not bunyip-api. bunyip-web had no matching route, so the request hit the `not_found` HTML fallback, and because the anchor carried a `download` attribute the browser saved that 404 page under the asset's filename. ## Fix bunyip-web is the browser-facing BFF: the session cookie is scoped to its origin and the browser must never call bunyip-api directly. This adds a streaming proxy route `GET /downloads/{slug}/{asset}` that re-auths the session, forwards the cookie to the API's membership/entitlement-gated download endpoint, and relays the bytes with the upstream `Content-Type` / `Content-Disposition`. `Api::get_stream` returns the raw `reqwest::Response` so a large asset streams through rather than buffering in memory. The asset anchor now targets this proxy on the web origin and drops the `download` attribute: success is driven by the proxy's `Content-Disposition`, and any non-2xx (expired session, lost entitlement, upstream outage) navigates the browser (sign-in on 401, the downloads page otherwise) rather than being saved as the file. ## OCI check (per the issue) OCI distribution is not affected. Container images are pulled by the docker client via the `docker login` / `docker pull` command blocks shown on the page, not a browser `download` anchor, so they never resolve against the web origin's HTML fallback. No change needed there. ## Test `just check-container` green: fmt + clippy (`-D warnings`) + workspace lib tests all pass. #BUNYIP-64
fix(web): proxy binary downloads through the BFF (BUNYIP-64)
All checks were successful
Check / fmt / clippy / build / test (pull_request) Successful in 1m1s
1e0c3ead83
The download link pointed at the API path `/v1/applications/{slug}/downloads/{asset}`, a root-relative URL that the browser resolved against the bunyip-web origin instead of bunyip-api. bunyip-web had no such route, so the request fell through to the `not_found` HTML fallback, and because the anchor carried a `download` attribute the browser saved that HTML 404 page under the asset's filename. That is the "downloads download an HTML file" symptom (e.g. the vervain-agent binary came down as HTML).

bunyip-web is the browser-facing BFF: the session cookie is scoped to its origin and the browser must never call bunyip-api directly. Add a streaming proxy route `GET /downloads/{slug}/{asset}` that re-auths the session, forwards the cookie to the API's gated download endpoint, and relays the bytes with the upstream Content-Type / Content-Disposition. `Api::get_stream` returns the raw `reqwest::Response` so a large asset streams through rather than buffering in memory.

The asset anchor now targets this proxy on the web origin and no longer sets a `download` attribute: success is driven by the proxy's Content-Disposition, and a non-2xx response (expired session, lost entitlement, upstream outage) navigates the browser (sign-in on 401, the downloads page otherwise) instead of being saved as the file.

OCI distribution is unaffected: container images are pulled by the docker client via the `docker login` / `docker pull` command blocks, not a browser `download` anchor, so they never hit the web origin's HTML fallback.

#BUNYIP-64

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(web): forward status + Content-Length on download proxy (BUNYIP-64)
All checks were successful
Check / fmt / clippy / build / test (pull_request) Successful in 1m28s
Create release / Create release from merged PR (pull_request) Has been skipped
20affa4293
Code-review follow-ups on the download proxy. Forward the upstream HTTP status instead of hardcoding 200, and relay the upstream Content-Length so the browser shows accurate download progress. The CompressionLayer drops the stale length itself when it compresses for the browser, and on the identity path the forwarded length is correct, so this is safe alongside the streaming body.

Harden the Content-Disposition fallback (a dead path in practice, since the API always sends the header) by escaping backslash and double-quote in the asset name so a stray character cannot break out of the quoted filename. A newline is already rejected by HeaderValue, which fails the response build into the existing redirect.

#BUNYIP-64

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch fix/bunyip-64-download-asset-bff-proxy 2026-06-09 20:00:20 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!82
No description provided.