Skip to content

Commit 732cf3f

Browse files
committed
new parseRangeHeader function and tests
1 parent d1a6768 commit 732cf3f

File tree

9 files changed

+357
-99
lines changed

9 files changed

+357
-99
lines changed

packages/bunshine/src/HttpRouter/HttpRouter.spec.ts

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type SseTestEvent = {
1414
const server: Server = {};
1515

1616
describe('HttpRouter', () => {
17+
let port = 7500;
1718
describe('handlers', () => {
1819
let app: HttpRouter;
1920
let oldEnv: string | undefined;
@@ -295,28 +296,131 @@ describe('HttpRouter', () => {
295296
expect(await resp2.text()).toBe('Hi');
296297
});
297298
});
298-
describe('server', () => {
299-
let app: HttpRouter;
299+
describe('listening', () => {
300300
let server: Server;
301-
beforeEach(() => {
302-
app = new HttpRouter();
303-
});
304301
afterEach(() => {
305302
server.stop(true);
306303
});
307304
it('should assign random port', async () => {
305+
const app = new HttpRouter();
306+
server = app.listen();
308307
app.get('/', () => new Response('Hi'));
309-
server = app.listen({ port: 7769 });
310-
const resp = await fetch(`http://localhost:${server.port}/`);
308+
const resp = await fetch(server.url);
311309
expect(typeof server.port).toBe('number');
312310
expect(server.port).toBeGreaterThan(0);
313311
expect(resp.status).toBe(200);
314312
expect(await resp.text()).toBe('Hi');
315313
});
314+
});
315+
describe('server', () => {
316+
let app: HttpRouter;
317+
let server: Server;
318+
beforeEach(() => {
319+
app = new HttpRouter();
320+
server = app.listen({ port: port++ });
321+
});
322+
afterEach(() => {
323+
server.stop(true);
324+
});
325+
it('should return correct statuses, headers, and bytes', async () => {
326+
app.headGet('/bun-logo.jpg', c => {
327+
return c.file(`${import.meta.dirname}/../../testFixtures/bun-logo.jpg`);
328+
});
329+
const url = `${server.url}/bun-logo.jpg`;
330+
331+
// Step 1: Fetch entire file
332+
const fullResponse = await fetch(url);
333+
const fullFileBytes = await fullResponse.blob();
334+
const fileSize = Number(fullResponse.headers.get('content-length'));
335+
336+
expect(fullResponse.status).toBe(200);
337+
expect(fullFileBytes.size).toBe(fileSize);
338+
expect(fullResponse.headers.get('accept-ranges')).toBe('bytes');
339+
expect(fullResponse.headers.get('content-type')).toBe('image/jpeg');
340+
341+
// Step 2: Fetch HEAD and validate
342+
const headResponse = await fetch(url, { method: 'HEAD' });
343+
344+
expect(headResponse.status).toBe(200);
345+
expect(headResponse.headers.get('content-length')).toBe(String(fileSize));
346+
expect(headResponse.headers.get('accept-ranges')).toBe('bytes');
347+
348+
// Step 3: Fetch range "bytes=0-" and validate
349+
const rangeResponse1 = await fetch(url, {
350+
headers: { Range: 'bytes=0-' },
351+
});
352+
const range1Bytes = await rangeResponse1.blob();
353+
354+
expect(rangeResponse1.status).toBe(206);
355+
expect(rangeResponse1.headers.get('accept-ranges')).toBe('bytes');
356+
expect(rangeResponse1.headers.get('content-type')).toBe('image/jpeg');
357+
expect(range1Bytes.size).toBe(fileSize);
358+
expect(rangeResponse1.headers.get('content-length')).toBe(
359+
String(fileSize)
360+
);
361+
expect(range1Bytes).toEqual(fullFileBytes);
362+
363+
// Step 4: Fetch range "bytes=0-999" and validate
364+
const rangeResponse2 = await fetch(url, {
365+
headers: { Range: 'bytes=0-999' },
366+
});
367+
const range2Bytes = await rangeResponse2.blob();
368+
369+
expect(rangeResponse2.status).toBe(206);
370+
expect(rangeResponse2.headers.get('accept-ranges')).toBe('bytes');
371+
expect(rangeResponse2.headers.get('content-length')).toBe('1000');
372+
expect(range2Bytes.size).toBe(1000);
373+
expect(rangeResponse2.headers.get('content-range')).toBe(
374+
`bytes 0-999/${fileSize}`
375+
);
376+
expect(range2Bytes).toEqual(fullFileBytes.slice(0, 1000));
377+
expect(rangeResponse2.headers.get('content-type')).toBe('image/jpeg');
378+
379+
// Step 5: Fetch range "bytes=1000-1999" and validate
380+
const rangeResponse3 = await fetch(url, {
381+
headers: { Range: 'bytes=1000-1999' },
382+
});
383+
const range3Bytes = await rangeResponse3.blob();
384+
385+
expect(rangeResponse3.status).toBe(206);
386+
expect(rangeResponse3.headers.get('accept-ranges')).toBe('bytes');
387+
expect(rangeResponse3.headers.get('content-length')).toBe('1000');
388+
expect(range3Bytes.size).toBe(1000);
389+
expect(rangeResponse3.headers.get('content-range')).toBe(
390+
`bytes 1000-1999/${fileSize}`
391+
);
392+
expect(range3Bytes).toEqual(fullFileBytes.slice(1000, 2000));
393+
expect(rangeResponse3.headers.get('content-type')).toBe('image/jpeg');
394+
395+
// Step 5: Fetch range "bytes=-1000" and validate
396+
const rangeResponse4 = await fetch(url, {
397+
headers: { Range: 'bytes=-1000' },
398+
});
399+
const range4Bytes = await rangeResponse4.blob();
400+
401+
expect(rangeResponse4.status).toBe(206);
402+
expect(rangeResponse4.headers.get('accept-ranges')).toBe('bytes');
403+
expect(rangeResponse4.headers.get('content-length')).toBe('1000');
404+
expect(range4Bytes.size).toBe(1000);
405+
expect(rangeResponse4.headers.get('content-range')).toBe(
406+
`bytes ${fileSize - 1001}-${fileSize - 1}/${fileSize}`
407+
);
408+
expect(range4Bytes).toEqual(fullFileBytes.slice(-1000));
409+
expect(rangeResponse4.headers.get('content-type')).toBe('image/jpeg');
410+
411+
// Step 7: Request invalid range
412+
const rangeResponse5 = await fetch(url, {
413+
headers: { Range: 'bytes=9999999-' },
414+
});
415+
expect(rangeResponse5.status).toBe(416);
416+
expect(rangeResponse5.statusText).toBe('Range Not Satisfiable');
417+
expect(rangeResponse5.headers.get('content-range')).toBe(
418+
`bytes */${fileSize}`
419+
);
420+
});
316421
it('should get client ip info', async () => {
317422
app.get('/', c => c.json(c.ip));
318-
server = app.listen({ port: 7770 });
319-
const resp = await fetch(`http://localhost:${server.port}/`);
423+
const resp = await fetch(server.url);
320424
const info = (await resp.json()) as {
321425
address: string;
322426
family: string;
@@ -328,33 +432,28 @@ describe('HttpRouter', () => {
328432
});
329433
it('should emit url', async () => {
330434
app.all('/', () => new Response('Hi'));
331-
server = app.listen({ port: 7771 });
332-
let output: string;
435+
let output: string = '';
333436
const to = (message: string) => (output = message);
334437
app.emitUrl({ to });
335-
// @ts-expect-error
336438
expect(output).toContain(String(server.url));
337439
});
338440
it('should handle all', async () => {
339441
app.all('/', () => new Response('Hi'));
340-
server = app.listen({ port: 7772 });
341-
const resp = await fetch('http://localhost:7772/');
442+
const resp = await fetch(server.url);
342443
expect(resp.status).toBe(200);
343444
expect(await resp.text()).toBe('Hi');
344445
});
345446
it('should handle GET', async () => {
346447
app.get('/', () => new Response('Hi'));
347-
server = app.listen({ port: 7773 });
348-
const resp = await fetch('http://localhost:7773/');
448+
const resp = await fetch(server.url);
349449
expect(resp.status).toBe(200);
350450
expect(await resp.text()).toBe('Hi');
351451
});
352452
it('should handle PUT', async () => {
353453
app.put('/', async ({ request, json }) => {
354454
return json(await request.json());
355455
});
356-
server = app.listen({ port: 7774 });
357-
const resp = await fetch('http://localhost:7774/', {
456+
const resp = await fetch(server.url, {
358457
method: 'PUT',
359458
headers: {
360459
'Content-Type': 'application/json',
@@ -376,8 +475,7 @@ describe('HttpRouter', () => {
376475
},
377476
});
378477
});
379-
server = app.listen({ port: 7775 });
380-
const resp = await fetch('http://localhost:7775/hi?name=Bob', {
478+
const resp = await fetch(`${server.url}/hi?name=Bob`, {
381479
method: 'HEAD',
382480
});
383481
expect(resp.status).toBe(204);
@@ -394,10 +492,9 @@ describe('HttpRouter', () => {
394492
},
395493
});
396494
});
397-
server = app.listen({ port: 7776 });
398495
const formData = new URLSearchParams();
399496
formData.append('key', 'secret');
400-
const resp = await fetch('http://localhost:7776/parrot', {
497+
const resp = await fetch(`${server.url}/parrot`, {
401498
method: 'POST',
402499
headers: {
403500
'Content-type': 'application/x-www-form-urlencoded',
@@ -418,10 +515,9 @@ describe('HttpRouter', () => {
418515
},
419516
});
420517
});
421-
server = app.listen({ port: 7777 });
422518
const formData = new FormData();
423519
formData.append('key2', 'secret2');
424-
const resp = await fetch('http://localhost:7777/parrot', {
520+
const resp = await fetch(`${server.url}/parrot`, {
425521
method: 'POST',
426522
body: formData,
427523
});
@@ -432,8 +528,7 @@ describe('HttpRouter', () => {
432528
app.patch('/', async ({ request, json }) => {
433529
return json(await request.json());
434530
});
435-
server = app.listen({ port: 7778 });
436-
const resp = await fetch('http://localhost:7778/', {
531+
const resp = await fetch(server.url, {
437532
method: 'PATCH',
438533
headers: {
439534
'Content-Type': 'application/json',
@@ -453,8 +548,7 @@ describe('HttpRouter', () => {
453548
},
454549
});
455550
});
456-
server = app.listen({ port: 7779 });
457-
const resp = await fetch('http://localhost:7779/', {
551+
const resp = await fetch(server.url, {
458552
method: 'TRACE',
459553
headers: {
460554
'Max-Forwards': '0',
@@ -474,8 +568,7 @@ describe('HttpRouter', () => {
474568
},
475569
});
476570
});
477-
server = app.listen({ port: 7780 });
478-
const resp = await fetch('http://localhost:7780/users/42', {
571+
const resp = await fetch(`${server.url}/users/42`, {
479572
method: 'DELETE',
480573
});
481574
expect(resp.status).toBe(204);
@@ -490,8 +583,7 @@ describe('HttpRouter', () => {
490583
},
491584
});
492585
});
493-
server = app.listen({ port: 7781 });
494-
const resp = await fetch('http://localhost:7781/users/42', {
586+
const resp = await fetch(`${server.url}/users/42`, {
495587
method: 'OPTIONS',
496588
});
497589
expect(resp.status).toBe(204);
@@ -501,9 +593,8 @@ describe('HttpRouter', () => {
501593
app.get('/home', ({ app }) => {
502594
return new Response(app.locals.foo);
503595
});
504-
server = app.listen({ port: 7782 });
505596
app.locals.foo = 'bar';
506-
const resp = await fetch('http://localhost:7782/home');
597+
const resp = await fetch(`${server.url}/home`);
507598
expect(resp.status).toBe(200);
508599
expect(await resp.text()).toBe('bar');
509600
});
@@ -573,7 +664,7 @@ describe('HttpRouter', () => {
573664
it('should handle unnamed data', async () => {
574665
const events = await sseTest({
575666
event: 'message',
576-
port: 7784,
667+
port: port++,
577668
payloads: [['Hello'], ['World']],
578669
});
579670
expect(events.length).toBe(2);
@@ -583,7 +674,7 @@ describe('HttpRouter', () => {
583674
it('should send last event id and origin', async () => {
584675
const events = await sseTest({
585676
event: 'myEvent',
586-
port: 7785,
677+
port: port++,
587678
payloads: [
588679
['myEvent', 'hi1', 'id1'],
589680
['myEvent', 'hi2', 'id2'],
@@ -594,13 +685,13 @@ describe('HttpRouter', () => {
594685
expect(events[1].data).toBe('hi2');
595686
expect(events[0].lastEventId).toBe('id1');
596687
expect(events[1].lastEventId).toBe('id2');
597-
expect(events[0].origin).toBe('http://localhost:7785');
598-
expect(events[1].origin).toBe('http://localhost:7785');
688+
expect(events[0].origin).toBe(`http://localhost:${port - 1}`);
689+
expect(events[1].origin).toBe(`http://localhost:${port - 1}`);
599690
});
600691
it('should JSON encode data if needed', async () => {
601692
const events = await sseTest({
602693
event: 'myEvent',
603-
port: 7786,
694+
port: port++,
604695
payloads: [['myEvent', { name: 'Bob' }]],
605696
});
606697
expect(events.length).toBe(1);
@@ -610,7 +701,7 @@ describe('HttpRouter', () => {
610701
spyOn(console, 'warn').mockImplementation(() => {});
611702
await sseTest({
612703
event: 'myEvent',
613-
port: 7787,
704+
port: port++,
614705
payloads: [['myEvent', { name: 'Bob' }]],
615706
headers: {
616707
'Content-Type': 'text/plain',
@@ -626,7 +717,7 @@ describe('HttpRouter', () => {
626717
spyOn(console, 'warn').mockImplementation(() => {});
627718
await sseTest({
628719
event: 'myEvent',
629-
port: 7788,
720+
port: port++,
630721
payloads: [['myEvent', { name: 'Bob' }]],
631722
headers: {
632723
'Content-Type': 'text/event-stream',

packages/bunshine/src/getMimeType/getMimeType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import { BunFile } from 'bun';
33
export default function getMimeType(file: BunFile) {
44
// Currently, we let Bun.file handle looking up mime types
55
// So far Bun has all the types you'd expect
6-
return file.type;
6+
return file.type || 'application/octet-stream';
77
}

packages/bunshine/src/middleware/serveFiles/serveFiles.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from 'path';
22
import type { Middleware } from '../../HttpRouter/HttpRouter';
33
import ms from '../../ms/ms';
4-
import buildFileResponse from '../../responseFactories/buildFileResponse';
54

65
// see https://expressjs.com/en/4x/api.html#express.static
76
// and https://www.npmjs.com/package/send#dotfiles
@@ -74,14 +73,7 @@ export function serveFiles(
7473
}
7574
return new Response('404 Not Found', { status: 404 });
7675
}
77-
const rangeHeader = c.request.headers.get('range');
78-
const response = await buildFileResponse({
79-
file,
80-
acceptRanges,
81-
chunkSize: 0,
82-
rangeHeader,
83-
method: c.request.method,
84-
});
76+
const response = await c.file(file);
8577
// add last modified
8678
if (lastModified) {
8779
response.headers.set(

0 commit comments

Comments
 (0)