Skip to content

feat: add HTTP Range request support for media streaming - #69

Merged
melvincarvalho merged 2 commits into
gh-pagesfrom
feature/range-requests
Jan 9, 2026
Merged

feat: add HTTP Range request support for media streaming#69
melvincarvalho merged 2 commits into
gh-pagesfrom
feature/range-requests

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

  • Enables video seeking and audio skipping via HTTP Range requests
  • Implements RFC 7233 byte-range requests with 206 Partial Content responses
  • Uses streaming for efficient partial reads (no need to load full file into memory)

Changes

  • src/handlers/resource.js: Add parseRangeHeader() helper and range request handling in handleGet
  • src/ldp/headers.js: Add Accept-Ranges: bytes header, update CORS headers
  • src/storage/filesystem.js: Add createReadStream() for streaming partial content

Supported Range Formats

  • bytes=0-1023 - first 1024 bytes
  • bytes=1024- - from byte 1024 to end
  • bytes=-500 - last 500 bytes

Test Plan

  • Verify video files can be seeked in browser
  • Verify audio files can skip forward/backward
  • Verify 416 response for invalid ranges
  • Verify all existing tests pass (213/213 passing)

Fixes #68

Enables video seeking and audio skipping by implementing HTTP/1.1 range requests:
- Add parseRangeHeader() to parse bytes=start-end, bytes=start-, bytes=-suffix formats
- Return 206 Partial Content with Content-Range header for valid ranges
- Return 416 Range Not Satisfiable for invalid ranges
- Add Accept-Ranges: bytes header to all non-container responses
- Use fs.createReadStream with start/end options for efficient partial reads
- Add Content-Range to CORS exposed headers and Range to allowed headers

Fixes #68

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements HTTP Range request support for media streaming, enabling video seeking and audio skipping functionality. The implementation follows RFC 7233 to handle byte-range requests with 206 Partial Content responses, using efficient streaming instead of loading full files into memory.

Key Changes:

  • Added parseRangeHeader() function to parse and validate Range request headers with support for three range formats (standard, open-ended, and suffix ranges)
  • Implemented streaming-based partial content delivery using Node.js read streams with configurable start/end byte positions
  • Updated HTTP headers to advertise Range support via Accept-Ranges: bytes and extended CORS configuration

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
src/handlers/resource.js Adds Range header parsing logic and 206/416 response handling for partial content requests in the GET handler
src/ldp/headers.js Advertises byte-range support via Accept-Ranges header and updates CORS headers to allow Range requests and expose Content-Range responses
src/storage/filesystem.js Introduces createReadStream() function for efficient streaming of partial file content based on byte ranges

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/handlers/resource.js Outdated
@@ -1,4 +1,5 @@
import * as storage from '../storage/filesystem.js';
import { createReadStream } from '../storage/filesystem.js';

Copilot AI Jan 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createReadStream function is imported separately on line 2, but it's already part of the storage namespace imported on line 1. This creates a redundant import. Consider using storage.createReadStream instead and removing this duplicate import.

Suggested change
import { createReadStream } from '../storage/filesystem.js';

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js
Comment on lines +300 to +334
// Handle Range requests for media files (video, audio, etc.)
const rangeHeader = request.headers.range;
if (rangeHeader && !isRdfContentType(storedContentType)) {
const range = parseRangeHeader(rangeHeader, stats.size);

if (range) {
const { start, end } = range;
const chunkSize = end - start + 1;

const headers = getAllHeaders({
isContainer: false,
etag: stats.etag,
contentType: storedContentType,
origin,
resourceUrl,
connegEnabled
});
headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`;
headers['Content-Length'] = chunkSize;
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);

Object.entries(headers).forEach(([k, v]) => reply.header(k, v));

const streamResult = createReadStream(storagePath, { start, end });
if (!streamResult) {
return reply.code(500).send({ error: 'Stream error' });
}

return reply.code(206).send(streamResult.stream);
} else {
// Range not satisfiable
reply.header('Content-Range', `bytes */${stats.size}`);
return reply.code(416).send({ error: 'Range Not Satisfiable' });
}
}

Copilot AI Jan 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new range request functionality lacks test coverage. Consider adding tests to verify: (1) correct parsing of all range formats (bytes=0-1023, bytes=1024-, bytes=-500), (2) proper 206 response with correct Content-Range headers, (3) 416 response for invalid ranges, (4) correct handling of ranges that exceed file size, and (5) stream error handling.

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js Outdated
});
headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`;
headers['Content-Length'] = chunkSize;
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);

Copilot AI Jan 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Vary header on line 319 overrides the one set by getAllHeaders on line 309. The getVaryHeader function only includes 'Accept, Origin' or 'Origin', but it's missing 'Authorization' which is important for caching - responses can vary based on authorization. This could cause caching issues where authenticated and unauthenticated responses get confused. Consider not overriding the Vary header that getAllHeaders already sets correctly.

Suggested change
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js
if (!streamResult) {
return reply.code(500).send({ error: 'Stream error' });
}

Copilot AI Jan 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream errors that occur after the stream is passed to reply.send() are not handled. If the file is deleted or becomes unreadable while streaming, the error will not be caught, potentially causing the response to hang or fail silently. Consider adding an error handler to the stream before sending it in the response.

Suggested change
streamResult.stream.on('error', (err) => {
// Log the error if a logger is available
if (request && request.log && typeof request.log.error === 'function') {
request.log.error({ err }, 'Error while streaming resource');
}
// Avoid sending another response if one has already been sent
if (!reply.sent) {
reply.code(500).send({ error: 'Stream error' });
}
});

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js
Comment on lines +48 to +50
if (parts.length !== 2) {
return null;
}

Copilot AI Jan 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation rejects multiple range requests (e.g., 'bytes=0-100,200-300') with a 416 Range Not Satisfiable response. According to RFC 7233, if you don't support multipart ranges but at least one range is satisfiable, you should either serve the first range or ignore the Range header and return 200 with full content. Consider handling this edge case by ignoring the Range header for multipart requests instead of returning 416.

Copilot uses AI. Check for mistakes.
Comment thread src/storage/filesystem.js
Comment on lines +62 to +66

try {
const stream = fs.createReadStream(filePath, options);
return { stream, filePath };
} catch {

Copilot AI Jan 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try-catch block in createReadStream will not catch file read errors because fs.createReadStream doesn't throw synchronously. Instead, it returns a stream that emits error events asynchronously. This function will always return a stream object (even for non-existent files) rather than null, which breaks the error handling in the caller at line 324 of resource.js. Consider checking if the file exists before creating the stream, or handle stream errors in the caller.

Suggested change
try {
const stream = fs.createReadStream(filePath, options);
return { stream, filePath };
} catch {
// Ensure the file exists before creating a read stream so we can
// reliably return null for missing resources.
if (!fs.pathExistsSync(filePath)) {
return null;
}
try {
const stream = fs.createReadStream(filePath, options);
return { stream, filePath };
} catch {
// Handle rare cases where createReadStream throws synchronously

Copilot uses AI. Check for mistakes.
- Remove redundant createReadStream import (use storage.createReadStream)
- Check file existence before creating stream (sync check)
- Add stream error handler for errors during response
- Ignore multi-range requests per RFC 7233 (serve full content)
- Don't override Vary header (getAllHeaders sets it correctly)
- Add comprehensive test suite for range requests (10 tests)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: add HTTP Range request support for video/audio seeking

2 participants