From ae18b98c1dfcac0f733176ccb19cdff23b667b75 Mon Sep 17 00:00:00 2001 From: Dimava Date: Sun, 23 Jun 2024 22:21:36 +0300 Subject: [PATCH 1/2] feat: follow Range header --- src/index.ts | 77 +++++++++++++++++++++++++++++++++++++--------- test/index.test.ts | 20 ++++++++++++ 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2ec99c2..f1bc82d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,26 @@ const listFiles = async (dir: string): Promise => { return all.flat() } +const createRangeResponse = async ( + file: Blob, + range: string, + headers: Record +) => { + // from https://bun.sh/docs/api/http#streaming-files + // parse `Range` header + const [start = 0, end = Infinity] = range + .split('=') // ["Range: bytes", "0-100"] + .at(-1)! // "0-100" + .split('-') // ["0", "100"] + .map(Number) // [0, 100] + + headers['Content-Range'] = `bytes ${start}-${end}/${file.size}` + return new Response(file.slice(start, end), { + headers, + status: 206 + }) +} + export const staticPlugin = async ( { assets = 'public', @@ -236,12 +256,22 @@ export const staticPlugin = async ( ? prefix + fileName.split(sep).join(URL_PATH_SEP) : join(prefix, fileName) + let responseSingleton: Response + app.get( pathName, noCache - ? new Response(file, { - headers - }) + ? ({ headers: reqHeaders }) => { + return !reqHeaders.range + ? (responseSingleton ??= new Response(file, { + headers + })) + : createRangeResponse( + file, + reqHeaders['range'], + headers + ) + } : async ({ headers: reqHeaders }) => { if (await isCached(reqHeaders, etag, filePath)) { return new Response(null, { @@ -255,9 +285,15 @@ export const staticPlugin = async ( if (maxAge !== null) headers['Cache-Control'] += `, max-age=${maxAge}` - return new Response(file, { - headers - }) + return !reqHeaders.range + ? new Response(file, { + headers + }) + : createRangeResponse( + file, + reqHeaders['range'], + headers + ) } ) @@ -279,9 +315,8 @@ export const staticPlugin = async ( headers['Etag'] = etag headers['Cache-Control'] = directive if (maxAge !== null) - headers[ - 'Cache-Control' - ] += `, max-age=${maxAge}` + headers['Cache-Control'] += + `, max-age=${maxAge}` return new Response(file, { headers @@ -358,9 +393,15 @@ export const staticPlugin = async ( } if (noCache) - return new Response(file, { - headers - }) + return !reqHeaders.range + ? new Response(file, { + headers + }) + : createRangeResponse( + file, + reqHeaders['range'], + headers + ) const etag = await generateETag(file) if (await isCached(reqHeaders, etag, path)) @@ -374,9 +415,15 @@ export const staticPlugin = async ( if (maxAge !== null) headers['Cache-Control'] += `, max-age=${maxAge}` - return new Response(file, { - headers - }) + return !reqHeaders.range + ? new Response(file, { + headers + }) + : createRangeResponse( + file, + reqHeaders['range'], + headers + ) } catch (error) { throw new NotFoundError() } diff --git a/test/index.test.ts b/test/index.test.ts index 388d44b..a1b5a04 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -437,4 +437,24 @@ describe('Static Plugin', () => { res = await app.handle(req('/public/html')) expect(res.status).toBe(404) }) + + it.each([ + [{}], + [{ alwaysStatic: true }], + [{ noCache: true }], + [{ alwaysStatic: true, noCache: true }] + ])('should work with range requests', async (options) => { + const app = new Elysia().use(staticPlugin(options)) + await app.modules + + const request = req('/public/takodachi.png') + request.headers.append('Range', 'bytes=100-200') + + const res = await app.handle(request) + expect(res.status).toBe(206) + expect(res.headers.get('Content-Range')).toBe('bytes 100-200/71118') + + const body = await res.blob() + expect(body.size).toBe(100) + }) }) From 13e60e8a7c2247f7300a8c587f1ff4c25dffcc81 Mon Sep 17 00:00:00 2001 From: Dimava Date: Sun, 23 Jun 2024 23:23:44 +0300 Subject: [PATCH 2/2] fix: off-by-one response header error --- src/index.ts | 2 +- test/index.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index f1bc82d..8300eaa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,7 +65,7 @@ const createRangeResponse = async ( .split('-') // ["0", "100"] .map(Number) // [0, 100] - headers['Content-Range'] = `bytes ${start}-${end}/${file.size}` + headers['Content-Range'] = `bytes ${start}-${end - 1}/${file.size}` return new Response(file.slice(start, end), { headers, status: 206 diff --git a/test/index.test.ts b/test/index.test.ts index a1b5a04..92bf533 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -452,7 +452,7 @@ describe('Static Plugin', () => { const res = await app.handle(request) expect(res.status).toBe(206) - expect(res.headers.get('Content-Range')).toBe('bytes 100-200/71118') + expect(res.headers.get('Content-Range')).toBe('bytes 100-199/71118') const body = await res.blob() expect(body.size).toBe(100)