Content-Length duplicated in headers for media files from s3 buckets

(#430) Bug Awaiting confirmation media self-hosting

Summary

# curl -sv http://127.0.0.1:8088/media/icons/1483528200372891649/3a5ab250.webp?size=160 -o /dev/null
*   Trying 127.0.0.1:8088...
* Connected to 127.0.0.1 (127.0.0.1) port 8088
> GET /media/icons/1483528200372891649/3a5ab250.webp?size=160 HTTP/1.1
> Host: 127.0.0.1:8088
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< accept-ranges: bytes
< access-control-allow-origin: *
< cache-control: public, max-age=31536000
< content-length: 5190
< content-type: image/webp
< date: Thu, 19 Mar 2026 05:41:47 GMT
< expires: Fri, 19 Mar 2027 05:41:47 GMT
< last-modified: Thu, 19 Mar 2026 05:41:47 GMT
< vary: Accept-Encoding, Range
< x-request-id: 7b3ec127-4197-4b21-bc39-87dd66010b16
< Content-Length: 5190
< Connection: keep-alive
< Keep-Alive: timeout=5
<
{ [5190 bytes data]
* Connection #0 to host 127.0.0.1 left intact

Since the headers are invalid, the server refuses to serve the files. This is a self-hosted instance on the latest pull of /refactor.

Steps to reproduce

  1. upload any file on a self hosted instance, for example an avatar
  2. watch the browser console for a 502 error.
  3. check the response header and note the duplicate content-length.

2 comments

Sign in with Fluxer to comment and vote.
Comment by Rex
RexSystem 1 vote
Status changed from Fixed to Awaiting confirmation
This was closed in a bulk cleanup before Fluxer V2 without being checked or fixed. It may work now, so it is waiting for someone to confirm whether the bug still happens.
Comment by @treminaor
RexSystem 1 vote originally by @treminaor on GitHub OP
Here's the fix:

1. packages/media_proxy/src/lib/HttpUtils.tsx

Removed manual Content-Length headers from media responses. Before:
if (range) {
    const length = range.end - range.start + 1;
    ctx.status(206);
    ctx.header('Content-Length', length.toString());
    ctx.header('Content-Range', `bytes ${range.start}-${range.end}/${size}`);
} else {
    ctx.header('Content-Length', size.toString());
}
After:
if (range) {
    const length = range.end - range.start + 1;
    ctx.status(206);
    ctx.header('Content-Range', `bytes ${range.start}-${range.end}/${size}`);
} else {
}

2. packages/s3/src/s3/ObjectController.tsx

Partially removed manual Content-Length handling in the normal non-range GET path. From the current snippet, the non-range branch no longer sets Content-Length, but the range branch still does. Current relevant block:
if (result.contentRange) {
    headers['Content-Range'] = result.contentRange;
    headers['Content-Length'] = String(
        parseInt(result.contentRange.split('/')[0]!.split('-')[1]!, 10) -
            parseInt(result.contentRange.split('/')[0]!.split('-')[0]!.split(' ')[1]!, 10) +
            1,
    );
    ctx.status(206);
} else {
}
So the normal image GET path is fixed, but range responses may still need cleanup later.