From e47af034e6901df9e925cc63abfede8c5b219a67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:55:38 +0000 Subject: [PATCH 1/5] Initial plan From cf6a282e8f8e971f927ceb653f0d4cf3ac7af82e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:01:52 +0000 Subject: [PATCH 2/5] Implement HTTP Range support for static files - Add Range header parsing for static files - Support three range types: start-end, start-, and -suffix - Return 206 Partial Content with Content-Range header - Return 416 Range Not Satisfiable for invalid ranges - Add Accept-Ranges: bytes header to all static responses - Only process ranges for uncompressed files (COMPRESS_METHOD_NONE) - HEAD requests bypass Range logic as expected - Fully backward compatible with non-Range requests Co-authored-by: L3P3 <4629449+L3P3@users.noreply.github.com> --- public/test.txt | 3 ++ rtjscomp.js | 82 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 public/test.txt diff --git a/public/test.txt b/public/test.txt new file mode 100644 index 0000000..71c8e77 --- /dev/null +++ b/public/test.txt @@ -0,0 +1,3 @@ +0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +abcdefghijklmnopqrstuvwxyz +0123456789 diff --git a/rtjscomp.js b/rtjscomp.js index febbd32..d4c97b3 100755 --- a/rtjscomp.js +++ b/rtjscomp.js @@ -1532,19 +1532,87 @@ const request_handle = async (request, response, https) => { response.setHeader('Vary', 'Accept-Encoding'); } - if (file_compression === COMPRESS_METHOD_NONE) { - response.setHeader('Content-Length', file_stat.size); + // Handle Range requests only for uncompressed files + let range_start = 0; + let range_end = file_stat.size - 1; + let is_range_request = false; + + if ( + file_compression === COMPRESS_METHOD_NONE && + 'range' in request_headers && + !request_method_head + ) { + response.setHeader('Accept-Ranges', 'bytes'); + const range_header = request_headers['range']; + const range_match = range_header.match(/^bytes=(\d*)-(\d*)$/); + + if (range_match) { + const start = range_match[1]; + const end = range_match[2]; + + if (start || end) { + is_range_request = true; + + if (start && end) { + // Both start and end specified: bytes=10-20 + range_start = parseInt(start, 10); + range_end = parseInt(end, 10); + } else if (start) { + // Only start specified: bytes=10- + range_start = parseInt(start, 10); + range_end = file_stat.size - 1; + } else { + // Only end specified (suffix-byte-range): bytes=-500 + const suffix_length = parseInt(end, 10); + range_start = Math.max(0, file_stat.size - suffix_length); + range_end = file_stat.size - 1; + } + + // Validate range + if ( + range_start < 0 || + range_end >= file_stat.size || + range_start > range_end + ) { + response.setHeader( + 'Content-Range', + `bytes */${file_stat.size}` + ); + throw 416; + } + + response.statusCode = 206; + response.setHeader( + 'Content-Range', + `bytes ${range_start}-${range_end}/${file_stat.size}` + ); + response.setHeader('Content-Length', range_end - range_start + 1); + } + } } - else { - response.setHeader( - 'Content-Encoding', - COMPRESS_METHODS[file_compression] - ); + + if (!is_range_request) { + if (file_compression === COMPRESS_METHOD_NONE) { + response.setHeader('Accept-Ranges', 'bytes'); + response.setHeader('Content-Length', file_stat.size); + } + else { + response.setHeader( + 'Content-Encoding', + COMPRESS_METHODS[file_compression] + ); + } } if (request_method_head) { response.end(); } + else if (is_range_request) { + fs.createReadStream(path_real_send, { + start: range_start, + end: range_end + }).pipe(response); + } else { fs.createReadStream(path_real_send) .pipe(response); From 88027bc085e6693d1b5c46e8bce77dcecd14f6ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:03:50 +0000 Subject: [PATCH 3/5] Address code review feedback for Range support - Add comment clarifying only single ranges are supported (not multipart) - Remove redundant range_start < 0 validation check - Add error handling for file streams to prevent crashes - Multi-range requests now gracefully fall back to full file (200 OK) Co-authored-by: L3P3 <4629449+L3P3@users.noreply.github.com> --- public/test.txt | 3 --- rtjscomp.js | 23 ++++++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) delete mode 100644 public/test.txt diff --git a/public/test.txt b/public/test.txt deleted file mode 100644 index 71c8e77..0000000 --- a/public/test.txt +++ /dev/null @@ -1,3 +0,0 @@ -0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ -abcdefghijklmnopqrstuvwxyz -0123456789 diff --git a/rtjscomp.js b/rtjscomp.js index d4c97b3..434235a 100755 --- a/rtjscomp.js +++ b/rtjscomp.js @@ -1544,6 +1544,7 @@ const request_handle = async (request, response, https) => { ) { response.setHeader('Accept-Ranges', 'bytes'); const range_header = request_headers['range']; + // Only single range requests supported (not multipart ranges) const range_match = range_header.match(/^bytes=(\d*)-(\d*)$/); if (range_match) { @@ -1570,7 +1571,6 @@ const request_handle = async (request, response, https) => { // Validate range if ( - range_start < 0 || range_end >= file_stat.size || range_start > range_end ) { @@ -1608,14 +1608,27 @@ const request_handle = async (request, response, https) => { response.end(); } else if (is_range_request) { - fs.createReadStream(path_real_send, { + const stream = fs.createReadStream(path_real_send, { start: range_start, end: range_end - }).pipe(response); + }); + stream.on('error', err => { + log(`[error] ${path} stream: ${err.message}`); + if (!response.headersSent) { + throw 500; + } + }); + stream.pipe(response); } else { - fs.createReadStream(path_real_send) - .pipe(response); + const stream = fs.createReadStream(path_real_send); + stream.on('error', err => { + log(`[error] ${path} stream: ${err.message}`); + if (!response.headersSent) { + throw 500; + } + }); + stream.pipe(response); } } } From 8f6113466e51cd5995e79fcebcf127e79b5e1300 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:31:29 +0000 Subject: [PATCH 4/5] Refactor: eliminate code duplication in stream creation Use conditional object for createReadStream options instead of separate if/else branches. Pass {start, end} when is_range_request is true, otherwise pass empty object. Co-authored-by: L3P3 <4629449+L3P3@users.noreply.github.com> --- rtjscomp.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/rtjscomp.js b/rtjscomp.js index 434235a..30bc66b 100755 --- a/rtjscomp.js +++ b/rtjscomp.js @@ -1607,21 +1607,11 @@ const request_handle = async (request, response, https) => { if (request_method_head) { response.end(); } - else if (is_range_request) { - const stream = fs.createReadStream(path_real_send, { - start: range_start, - end: range_end - }); - stream.on('error', err => { - log(`[error] ${path} stream: ${err.message}`); - if (!response.headersSent) { - throw 500; - } - }); - stream.pipe(response); - } else { - const stream = fs.createReadStream(path_real_send); + const stream = fs.createReadStream( + path_real_send, + is_range_request ? {start: range_start, end: range_end} : {} + ); stream.on('error', err => { log(`[error] ${path} stream: ${err.message}`); if (!response.headersSent) { From ced18c63416a4b40342919ce49e5a10e52c4bd54 Mon Sep 17 00:00:00 2001 From: Len Date: Thu, 1 Jan 2026 19:34:54 +0100 Subject: [PATCH 5/5] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7d6561..9b94f2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rtjscomp", - "version": "0.9.11", + "version": "0.9.12", "description": "php-like server but with javascript", "repository": { "type": "git",