char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
26.3 KiB482 linesraw
1mod common;
2
3fn run(source: &str) -> std::process::Output {
4    let dir = tempfile::tempdir().unwrap();
5    std::fs::write(dir.path().join("hello.txt"), b"0123456789").unwrap();
6    std::fs::write(dir.path().join("empty.bin"), b"").unwrap();
7    std::fs::write(dir.path().join("large.bin"), vec![42; 200_000]).unwrap();
8    std::fs::write(
9        dir.path().join("pattern.bin"),
10        (0..200_000).map(|i| (i % 251) as u8).collect::<Vec<_>>(),
11    )
12    .unwrap();
13    std::fs::File::create(dir.path().join("huge.bin"))
14        .unwrap()
15        .set_len(64 * 1024 * 1024)
16        .unwrap();
17    common::run(
18        &format!(
19            "const root = {}; const rootURL = {};\n{source}",
20            serde_json::to_string(dir.path().to_str().unwrap()).unwrap(),
21            serde_json::to_string(url::Url::from_directory_path(dir.path()).unwrap().as_str())
22                .unwrap()
23        ),
24        &[],
25    )
26}
27
28#[test]
29fn handles_transfer_ownership_and_preserve_opened_identity() {
30    let output = run(r#"
31        import {serveFile, open, rename, errors, FileHandle} from 'avril:std';
32        const file = await open(root+'/hello.txt');
33        await file.seek(7, 0);
34        const readable = file.readable, writable = file.writable;
35        await rename(root+'/hello.txt', root+'/moved.bin');
36        const replacement = await open(root+'/hello.txt', {write:true, createNew:true});
37        await replacement.write(new TextEncoder().encode('replacement')); replacement.close();
38        const pending = serveFile(new Request('http://localhost/'), file);
39        try { file.statSync(); throw Error('handle still usable'); }
40        catch (error) { if (!(error instanceof errors.BadResource)) throw error; }
41        file[Symbol.dispose]();
42        const response = await pending;
43        if (!(response instanceof Response) || response.status !== 200 ||
44            response.headers.get('content-type') !== 'text/plain; charset=utf-8' ||
45            response.headers.get('content-length') !== '10' ||
46            await response.text() !== '0123456789') throw Error('opened file');
47        try { await readable.getReader().read(); throw Error('old readable still usable'); }
48        catch (error) { if (!(error instanceof errors.BadResource)) throw error; }
49        try { await writable.getWriter().write(new Uint8Array([1])); throw Error('old writable still usable'); }
50        catch (error) { if (!(error instanceof errors.BadResource)) throw error; }
51        try { await serveFile(new Request('http://localhost/'), file); throw Error('reused handle'); }
52        catch (error) { if (!(error instanceof errors.BadResource)) throw error; }
53        const forged = Object.create(FileHandle.prototype);
54        try { await serveFile(new Request('http://localhost/'), forged); throw Error('accepted forged handle'); }
55        catch (error) { if (!(error instanceof TypeError)) throw error; }
56        console.log('ownership');
57    "#);
58    assert_eq!(output.stdout, b"ownership\n");
59}
60
61#[test]
62fn retained_handles_support_concurrent_ranges_without_changing_the_cursor() {
63    let output = run(r#"
64        import {serveFile, open} from 'avril:std';
65        const file = await open(root+'/pattern.bin');
66        await file.seek(731, 0);
67        const intervals = [[0,199999], [65530,180000], [130001,199999]];
68        const responses = await Promise.all(intervals.map(([start,end]) =>
69            serveFile(new Request('http://localhost/', {headers:{range:`bytes=${start}-${end}`}}), file, {consume:false})
70        ));
71        if (await file.seek(0, 1) !== 731) throw Error('preparation changed cursor');
72        await Promise.all(responses.map(async (response, index) => {
73            const [start,end] = intervals[index];
74            if (response.status !== 206 || response.headers.get('content-range') !== `bytes ${start}-${end}/200000`) throw Error('range metadata');
75            const bytes = new Uint8Array(await response.arrayBuffer());
76            if (bytes.length !== end-start+1 || bytes.some((x,i) => x !== (start+i)%251)) throw Error('concurrent content');
77        }));
78        if (await file.seek(0, 1) !== 731) throw Error('streaming changed cursor');
79        const byte = new Uint8Array(1);
80        if (await file.read(byte) !== 1 || byte[0] !== 731%251) throw Error('original read');
81        const pending = serveFile(new Request('http://localhost/'), file, {consume:false});
82        file.close();
83        const response = await pending;
84        const bytes = new Uint8Array(await response.arrayBuffer());
85        if (bytes.length !== 200000 || bytes.some((x,i) => x !== i%251)) throw Error('original close interrupted response');
86        console.log('retained handles');
87    "#);
88    assert_eq!(output.stdout, b"retained handles\n");
89}
90
91#[test]
92fn handle_metadata_and_bodyless_responses_obey_ownership_options() {
93    let output = run(r#"
94        import {serveFile, open, errors} from 'avril:std';
95        const request = (headers = {}, method = 'GET') => new Request('http://localhost/', {headers, method});
96        const file = await open(root+'/hello.txt');
97        const first = await serveFile(request(), file, {consume:false, headers:{etag:'"version"', 'content-type':'application/custom'}});
98        if (first.headers.get('content-type') !== 'application/custom' || await first.text() !== '0123456789') throw Error('custom headers');
99        const cases = [
100            [{range:'bytes=2-3'}, 'HEAD', 200, '10'],
101            [{'if-none-match':'"version"'}, 'GET', 304, null],
102            [{'if-match':'"other"'}, 'GET', 412, '0'],
103            [{range:'bytes=100-'}, 'GET', 416, '0'],
104            [{}, 'POST', 405, '0'],
105        ];
106        for (const [headers,method,status,length] of cases) {
107            const options = {consume:false, headers:{etag:'"version"'}};
108            const response = await serveFile(request(headers,method), file, options);
109            if (response.status !== status || response.body !== null || response.headers.get('content-length') !== length) throw Error('retained status ' + status);
110            if ((await file.stat()).size !== 10) throw Error('closed retained handle');
111            const owned = await open(root+'/hello.txt');
112            const consumed = await serveFile(request(headers,method), owned, {headers:options.headers});
113            if (consumed.status !== status || consumed.body !== null) throw Error('consumed status ' + status);
114            try { await owned.stat(); throw Error('did not consume bodyless handle'); }
115            catch (error) { if (!(error instanceof errors.BadResource)) throw error; }
116        }
117        file.close();
118        const empty = await open(root+'/empty.bin');
119        const response = await serveFile(request(), empty);
120        if (response.status !== 200 || response.body !== null || response.headers.get('content-length') !== '0') throw Error('empty handle');
121        console.log('handle metadata');
122    "#);
123    assert_eq!(output.stdout, b"handle metadata\n");
124}
125
126#[test]
127fn handle_validation_cancellation_and_revocation() {
128    let output = run(r#"
129        import {serveFile, open, pledge, errors} from 'avril:std';
130        const request = () => new Request('http://localhost/');
131        const file = await open(root+'/hello.txt');
132        for (const options of [{consume:'false'}, {headers:{'bad header':'x'}}]) {
133            try { await serveFile(request(), file, options); throw Error('accepted invalid options'); }
134            catch (error) { if (!(error instanceof TypeError)) throw error; }
135            await file.stat();
136        }
137        const control = new AbortController(); control.abort(Error('stop'));
138        try { await serveFile(new Request('http://localhost/', {signal:control.signal}), file); throw Error('ignored early abort'); }
139        catch (error) { if (error.message !== 'stop') throw error; }
140        await file.stat();
141        const writeOnly = await open(root+'/hello.txt', {write:true});
142        try { await serveFile(request(), writeOnly); throw Error('accepted write-only handle'); }
143        catch (error) { if (!(error instanceof errors.BadResource)) throw error; }
144        await writeOnly.stat(); writeOnly.close();
145        file.close();
146        const shared = await open(root+'/large.bin');
147        const canceled = await serveFile(request(), shared, {consume:false});
148        await canceled.body.cancel();
149        await shared.stat();
150        const controller = new AbortController();
151        const aborted = await serveFile(new Request('http://localhost/', {signal:controller.signal}), shared, {consume:false});
152        controller.abort(Error('stop'));
153        try { await aborted.text(); throw Error('ignored abort'); }
154        catch (error) { if (error.message !== 'stop') throw error; }
155        await shared.stat();
156        const revoked = await serveFile(request(), shared, {consume:false});
157        pledge({read:false});
158        try { await revoked.text(); throw Error('read after revocation'); }
159        catch (error) { if (!(error instanceof errors.PermissionDenied)) throw error; }
160        try { await serveFile(request(), shared); throw Error('acquired after revocation'); }
161        catch (error) { if (!(error instanceof errors.PermissionDenied)) throw error; }
162        shared.close();
163        console.log('handle failures');
164    "#);
165    assert_eq!(output.stdout, b"handle failures\n");
166}
167
168#[test]
169fn handle_responses_use_the_native_http_path_and_retained_bindings() {
170    let output = run(r#"
171        import {serve, serveFile, open, FileHandle} from 'avril:std';
172        const file = await open(root+'/pattern.bin');
173        const poison = () => {throw Error('guest method');};
174        FileHandle.prototype.read = FileHandle.prototype.stat = FileHandle.prototype.seek =
175            FileHandle.prototype.close = FileHandle.prototype[Symbol.dispose] = poison;
176        let transferred;
177        const server = serve({port:0}, async request => {
178            const owned = request.url.endsWith('/owned');
179            const response = await serveFile(request, owned ? await open(root+'/pattern.bin') : file, {consume:owned});
180            transferred = response;
181            return response;
182        });
183        const url = `http://127.0.0.1:${server.addr.port}`;
184        await Promise.all(['/shared-a','/shared-b','/owned'].map(async path => {
185            const response = await fetch(url+path, {headers:{range:'bytes=65530-180000'}});
186            const bytes = new Uint8Array(await response.arrayBuffer());
187            if (response.status !== 206 || bytes.length !== 114471 || bytes.some((x,i) => x !== (65530+i)%251)) throw Error('wire range');
188        }));
189        if (!transferred.bodyUsed) throw Error('native body not transferred');
190        await server.shutdown();
191        console.log('native handles');
192    "#);
193    assert_eq!(output.stdout, b"native handles\n");
194}
195
196#[test]
197fn metadata_head_and_conditional_requests() {
198    let output = run(r#"
199        import {serveFile} from 'avril:std';
200        const path = root + '/hello.txt';
201        const req = (headers = {}, method = 'GET') => new Request('http://localhost/', {headers, method});
202        const first = await serveFile(req(), path);
203        const etag = first.headers.get('etag'), modified = first.headers.get('last-modified');
204        if (!etag || !modified || first.headers.get('content-length') !== '10' ||
205            first.headers.get('content-type') !== 'text/plain; charset=utf-8' ||
206            first.headers.get('accept-ranges') !== 'bytes' || await first.text() !== '0123456789') throw Error('metadata');
207        const head = await serveFile(req({range:'bytes=2-3'}, 'HEAD'), path);
208        if (head.status !== 200 || head.body !== null || head.headers.get('content-length') !== '10' ||
209            head.headers.get('etag') !== etag) throw Error('HEAD');
210        const old = 'Thu, 01 Jan 1970 00:00:00 GMT';
211        const cases = [
212            [{'if-none-match':etag}, 304],
213            [{'if-none-match':etag.slice(2)}, 304],
214            [{'if-none-match':'"unrelated,tag", ' + etag}, 304],
215            [{'if-none-match':'*'}, 304],
216            [{'if-none-match':', , ' + etag + ','}, 304],
217            [{'if-modified-since':modified}, 304],
218            [{'if-modified-since':new Date(Date.now()+86400000).toUTCString()}, 304],
219            [{'if-modified-since':old}, 200],
220            [{'if-modified-since':'garbage'}, 200],
221            [{'if-none-match':'"other"', 'if-modified-since':modified}, 200],
222            [{'if-match':etag}, 412],
223            [{'if-match':etag.slice(2)}, 412],
224            [{'if-match':'*'}, 200],
225            [{'if-unmodified-since':old}, 412],
226            [{'if-unmodified-since':modified}, 200],
227            [{'if-match':'*', 'if-unmodified-since':old}, 200],
228            [{'if-match':'"other"', 'if-none-match':'*'}, 412],
229            [{'if-none-match':etag, range:'bytes=100-'}, 304],
230        ];
231        for (const [headers, status] of cases) {
232            const response = await serveFile(req(headers), path);
233            if (response.status !== status) throw Error(JSON.stringify(headers) + ': ' + response.status);
234            if (status !== 200 && (response.body !== null || response.headers.get('etag') !== etag)) throw Error('conditional body');
235            if (status === 304 && response.headers.has('content-length')) throw Error('304 length');
236            await response.text();
237        }
238        console.log('conditions');
239        "#);
240    assert_eq!(output.stdout, b"conditions\n");
241}
242
243#[test]
244fn byte_ranges_and_if_range() {
245    let output = run(r#"
246        import {serveFile} from 'avril:std';
247        const path = root + '/hello.txt';
248        const req = headers => new Request('http://localhost/', {headers});
249        const first = await serveFile(req({}), path);
250        const modified = first.headers.get('last-modified'), etag = first.headers.get('etag');
251        await first.body.cancel();
252        const cases = [
253            ['bytes=2-4', 206, '234', 'bytes 2-4/10'],
254            ['bytes=7-', 206, '789', 'bytes 7-9/10'],
255            ['bytes=-3', 206, '789', 'bytes 7-9/10'],
256            ['bytes=-100', 206, '0123456789', 'bytes 0-9/10'],
257            ['bytes=8-1000000000000000000000000000', 206, '89', 'bytes 8-9/10'],
258            ['bytes=-99999999999999999999999999', 206, '0123456789', 'bytes 0-9/10'],
259            ['bytes=10-', 416, '', 'bytes */10'],
260            ['bytes=99999999999999999999999999-', 416, '', 'bytes */10'],
261            ['bytes=-0', 416, '', 'bytes */10'],
262            ['bytes=4-2', 200, '0123456789', null],
263            ['bytes=9007199254740993-9007199254740992', 200, '0123456789', null],
264            ['bytes=-', 200, '0123456789', null],
265            ['items=0-1', 200, '0123456789', null],
266            ['bytes=0-1,4-5', 200, '0123456789', null],
267            ['garbage', 200, '0123456789', null],
268        ];
269        for (const [range, status, text, contentRange] of cases) {
270            const response = await serveFile(req({range}), path);
271            if (response.status !== status || response.headers.get('content-range') !== contentRange ||
272                response.headers.get('content-length') !== String(text.length) || await response.text() !== text) throw Error(range);
273        }
274        for (const [validator, status] of [[modified,206], [etag,200], [etag.slice(2),200],
275            ['"other"',200], ['invalid',200], ['Thu, 01 Jan 1970 00:00:00 GMT',200]]) {
276            const response = await serveFile(req({range:'bytes=1-2', 'if-range':validator}), path);
277            if (response.status !== status || await response.text() !== (status === 206 ? '12' : '0123456789')) throw Error('If-Range ' + validator);
278        }
279        const strong = {headers:{etag:'"known,version"', 'content-type':'application/custom',
280            'cache-control':'public, max-age=30', 'content-length':'12345', 'content-range':'invalid'}};
281        for (const headers of [{'if-match':'"known,version"'}, {range:'bytes=1-2', 'if-range':'"known,version"'}]) {
282            const response = await serveFile(req(headers), new URL('hello.txt', rootURL), strong);
283            if (response.status !== (headers.range ? 206 : 200) || response.headers.get('content-type') !== 'application/custom' ||
284                response.headers.get('cache-control') !== 'public, max-age=30' ||
285                response.headers.get('content-length') !== (headers.range ? '2' : '10')) throw Error('overrides');
286            await response.text();
287        }
288        const empty = await serveFile(req({range:'bytes=0-'}), root+'/empty.bin');
289        if (empty.status !== 416 || empty.headers.get('content-range') !== 'bytes */0') throw Error('empty range');
290        console.log('ranges');
291        "#);
292    assert_eq!(output.stdout, b"ranges\n");
293}
294
295#[test]
296fn streaming_abort_permissions_and_file_identity() {
297    let output = run(r#"
298        import {serveFile, open, rename, pledge, errors} from 'avril:std';
299        const request = () => new Request('http://localhost/');
300        const response = await serveFile(request(), root+'/hello.txt');
301        await rename(root+'/hello.txt', root+'/moved.txt');
302        if (await response.text() !== '0123456789') throw Error('opened identity');
303        const growing = await serveFile(request(), root+'/moved.txt');
304        const writer = await open(root+'/moved.txt', {append:true});
305        await writer.write(new TextEncoder().encode('extra')); writer.close();
306        if (await growing.text() !== '0123456789') throw Error('growth exceeded Content-Length');
307        const short = await serveFile(request(), root+'/moved.txt');
308        const shrink = await open(root+'/moved.txt', {write:true});
309        await shrink.truncate(2); shrink.close();
310        try { await short.text(); throw Error('accepted short file'); }
311        catch (error) { if (error.message === 'accepted short file') throw error; }
312        const control = new AbortController();
313        const aborted = await serveFile(new Request('http://localhost/', {signal:control.signal}), root+'/large.bin');
314        control.abort(Error('stop reading'));
315        try { await aborted.text(); throw Error('ignored abort'); }
316        catch (error) { if (error.message !== 'stop reading') throw error; }
317        try { await serveFile(new Request('http://localhost/', {signal:control.signal}), root+'/large.bin'); throw Error('ignored early abort'); }
318        catch (error) { if (error.message !== 'stop reading') throw error; }
319        const canceled = await serveFile(request(), root+'/large.bin');
320        await canceled.body.cancel();
321        const revoked = await serveFile(request(), root+'/large.bin');
322        pledge({read:false});
323        try { await revoked.text(); throw Error('read after revocation'); }
324        catch (error) { if (!(error instanceof errors.PermissionDenied)) throw error; }
325        try { await serveFile(request(), root+'/large.bin'); throw Error('open after revocation'); }
326        catch (error) { if (!(error instanceof errors.PermissionDenied)) throw error; }
327        console.log('streaming');
328        "#);
329    assert_eq!(output.stdout, b"streaming\n");
330}
331
332#[test]
333fn cloning_transforming_and_rewrapping_file_responses() {
334    let output = run(r#"
335        import {serve, serveFile} from 'avril:std';
336        const response = await serveFile(new Request('http://localhost/'), root+'/hello.txt');
337        const clone = response.clone();
338        if (await response.text() !== '0123456789' || await clone.text() !== '0123456789') throw Error('clone');
339        let transferred, incoming;
340        const server = serve({port:0}, async request => {
341            if (request.url.endsWith('/reuse')) return transferred;
342            const response = await serveFile(request, root+'/large.bin');
343            if (request.url.endsWith('/rewrap')) {
344                transferred = response; incoming = request;
345                return new Response(response.body, {headers:response.headers});
346            }
347            if (request.url.endsWith('/clone')) {
348                const clone = response.clone();
349                await response.arrayBuffer();
350                return clone;
351            }
352            if (request.url.endsWith('/transform')) {
353                return new Response(response.body.pipeThrough(new TransformStream({
354                    transform(bytes, controller) { controller.enqueue(bytes.map(x => x + 1)); },
355                })), {headers:response.headers});
356            }
357            response.headers.set('content-length', '1');
358            return response;
359        });
360        const url = `http://127.0.0.1:${server.addr.port}`;
361        for (const path of ['/rewrap', '/clone', '/transform']) {
362            const response = await fetch(url+path);
363            const bytes = new Uint8Array(await response.arrayBuffer());
364            if (response.status !== 200 || bytes.length !== 200000 || bytes.some(x => x !== (path === '/transform' ? 43 : 42))) throw Error(path);
365        }
366        if (!transferred.bodyUsed || incoming.signal.aborted) throw Error('transferred body lifecycle');
367        for (const path of ['/reuse', '/bad-length']) {
368            const response = await fetch(url+path);
369            if (response.status !== 500 || await response.text() !== 'Internal Server Error') throw Error('invalid ' + path);
370        }
371        await server.shutdown();
372        console.log('bodies');
373    "#);
374    assert_eq!(output.stdout, b"bodies\n");
375}
376
377#[test]
378fn revocation_and_independent_request_abort_interrupt_active_responses() {
379    let output = run(r#"
380        import {serve, serveFile, pledge} from 'avril:std';
381        let control;
382        const server = serve({port:0}, request => {
383            if (request.url.endsWith('/revoke')) { pledge({read:false}); return new Response('revoked'); }
384            if (request.url.endsWith('/independent')) {
385                control = new AbortController();
386                request = new Request(request.url, {signal:control.signal});
387            }
388            return serveFile(request, root+'/huge.bin');
389        });
390        const url = `http://127.0.0.1:${server.addr.port}`;
391        const independent = await fetch(url+'/independent');
392        const canceled = independent.body.getReader();
393        if ((await canceled.read()).done) throw Error('missing independent body');
394        control.abort(Error('stop'));
395        let failed = false;
396        try { while (!(await canceled.read()).done) {} } catch { failed = true; }
397        if (!failed) throw Error('ignored independent signal');
398        canceled.releaseLock();
399        const native = await fetch(url+'/native');
400        const reader = native.body.getReader();
401        let total = (await reader.read()).value.length;
402        if (await (await fetch(url+'/revoke')).text() !== 'revoked') throw Error('revocation');
403        failed = false;
404        try {
405            for (;;) {
406                const next = await reader.read();
407                if (next.done) break;
408                total += next.value.length;
409            }
410        } catch { failed = true; }
411        if (!failed || total >= 64 * 1024 * 1024) throw Error('read after revocation');
412        reader.releaseLock();
413        await server.shutdown();
414        console.log('interrupted');
415    "#);
416    assert_eq!(output.stdout, b"interrupted\n");
417}
418
419#[test]
420fn retained_bindings_and_byob_streaming() {
421    let output = run(r#"
422        import {serveFile, FileHandle} from 'avril:std';
423        const request = new Request('http://localhost/', {headers:{range:'bytes=65530-65540'}});
424        const poison = () => {throw Error('guest method');};
425        FileHandle.prototype.read = FileHandle.prototype.stat = FileHandle.prototype.seek =
426            FileHandle.prototype[Symbol.dispose] = poison;
427        Date.prototype.toUTCString = Math.floor = Number.isSafeInteger = poison;
428        globalThis.Date = globalThis.Headers = poison;
429        const response = await serveFile(request, root+'/large.bin');
430        const reader = response.body.getReader({mode:'byob'});
431        let count = 0;
432        for (;;) {
433            const next = await reader.read(new Uint8Array(3));
434            if (next.done) break;
435            for (const byte of next.value) if (byte !== 42) throw Error('BYOB content');
436            count += next.value.length;
437        }
438        if (count !== 11 || response.status !== 206) throw Error('BYOB length');
439        reader.releaseLock();
440        console.log('retained');
441        "#);
442    assert_eq!(output.stdout, b"retained\n");
443}
444
445#[test]
446fn responses_over_http_and_error_cases() {
447    let output = run(r#"
448        import {serve, serveFile} from 'avril:std';
449        const server = serve({port:0}, request => serveFile(request, root + new URL(request.url).pathname));
450        const url = `http://127.0.0.1:${server.addr.port}`;
451        const full = await fetch(url+'/large.bin');
452        if (full.status !== 200 || (await full.arrayBuffer()).byteLength !== 200000) throw Error('large response');
453        const part = await fetch(url+'/large.bin', {headers:{range:'bytes=65530-65540'}});
454        const bytes = new Uint8Array(await part.arrayBuffer());
455        if (part.status !== 206 || bytes.length !== 11 || bytes.some(x => x !== 42)) throw Error('large range');
456        const head = await fetch(url+'/large.bin', {method:'HEAD'});
457        if (head.body !== null || head.headers.get('content-length') !== '200000') throw Error('wire HEAD');
458        const notModified = await fetch(url+'/large.bin', {headers:{'if-none-match':head.headers.get('etag')}});
459        if (notModified.status !== 304 || notModified.body !== null) throw Error('wire 304');
460        const empty = await fetch(url+'/empty.bin');
461        if (empty.status !== 200 || empty.headers.get('content-type') !== 'application/octet-stream' || await empty.text() !== '') throw Error('empty');
462        for (const path of ['/missing', '/', '/hello.txt/child']) {
463            const response = await fetch(url+path);
464            if (response.status !== 404 || await response.text() !== '') throw Error('missing ' + path);
465        }
466        const post = await fetch(url+'/hello.txt', {method:'POST'});
467        if (post.status !== 405 || post.headers.get('allow') !== 'GET, HEAD') throw Error('method');
468        await post.text();
469        const canceled = await fetch(url+'/large.bin');
470        await canceled.body.cancel();
471        await server.shutdown();
472        for (const [method, path, status] of [['GET', '/missing', 404], ['POST', '/hello.txt', 405]]) {
473            const response = await serveFile(new Request(url, {method}), root+path, {
474                headers:{'access-control-allow-origin':'*', 'content-length':'999', 'content-range':'invalid'},
475            });
476            if (response.status !== status || response.headers.get('access-control-allow-origin') !== '*' ||
477                response.headers.get('content-length') !== '0' || response.headers.has('content-range')) throw Error('error headers');
478        }
479        console.log('http');
480        "#);
481    assert_eq!(output.stdout, b"http\n");
482}