SIGN IN SIGN UP

Support HTTP range requests for streamed files (#24517)

## Why now

#24518 adds a native `<video>` player for call recordings, and video
seeking runs on HTTP range requests. The file streaming endpoint
answered every request with `200` and the full body (no `Accept-Ranges`,
no `Content-Length`), so browsers report an empty seekable window
(`video.seekable = [0, 0]`) and clicking the timeline snaps playback
back to 0. Safari will not start playback at all without a `206` answer
to its range probe.

This only affects the streaming path: local storage (every dev
environment and default self-host), or S3 with presign disabled. On S3
with presign (Twenty Cloud), the endpoint 302-redirects to a presigned
URL and S3 serves ranges natively, so that path is behaviorally
untouched by this PR. Nothing served through the streaming path needed
seeking before (images, avatars, downloads), which is why range support
never came up.

## What it does

- `resolveByteRange` parses single-range `Range` headers (RFC 9110
subset); malformed or multi-range headers degrade to a normal full `200`
- `GET /files/file/:fileFolder/:id` answers `206` with
`Content-Range`/`Content-Length`, `416` when the range is unsatisfiable,
and advertises `Accept-Ranges: bytes`
- the normalized range flows through `FileStorageService.readFile` into
each driver: local uses `createReadStream({ start, end })`, S3 sets
`Range: bytes=a-b` on `GetObject` (covers the S3-without-presign config)
- presigned redirects, full-file streaming, and the other file endpoints
are unchanged

## Stack

- **#24517: HTTP range streaming (this PR)**
- #24518: native call-recording video player
- #24519: transcript/playback synchronization

Backend-only on purpose; the player and transcript sync layer on in the
next two PRs.

## Test plan

- unit: range-parsing matrix, driver range translation (local against a
real filesystem, S3 against the SDK command), controller 206/416 header
shaping
- integration (real app, local storage): bounded `206` byte-exact,
open-ended `bytes=10-` tail, malformed header degrading to full `200`,
out-of-range `416`
- `tsgo --noEmit` and `lint:diff-with-main` clean

Browser-level scrub verification is deferred to #24518; the presigned
path does not run this code, S3 answers ranges itself.
N
nitin committed
641797cfe3ad4fb8249cdff3e96c2c478fec58c0
Parent: fbce1f6
Committed by GitHub <noreply@github.com> on 8/24/2026, 1:53:02 PM