char/avril

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

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

Charlotte Somport http extension to typescript495c9a5

main
24.1 KiB539 linesraw
1mod common;
2
3use std::io::{BufRead, BufReader, Read, Write};
4use std::net::TcpStream;
5use std::time::Duration;
6
7#[test]
8fn request_response_streaming_and_concurrent_handlers() {
9    let output = common::run(
10        r#"
11        import {serve} from 'avril:std';
12        let unblock;
13        const blocked = new Promise(resolve => unblock = resolve);
14        const server = serve({port:0}, async (request, info) => {
15            if (info.remoteAddr.transport !== 'tcp' || info.remoteAddr.hostname !== '127.0.0.1') throw Error('remote address');
16            if (request.url.endsWith('/release')) { unblock(); return new Response('released'); }
17            await blocked;
18            if (request.method !== 'POST' || request.headers.get('x-test') !== 'request') throw Error('request metadata');
19            try { request.headers.set('x-test','changed'); throw Error('mutable'); }
20            catch (error) { if (error.message === 'mutable') throw error; }
21            const clone = request.clone();
22            if (await clone.text() !== 'firstsecond') throw Error('request clone');
23            return new Response(request.body, {status:201, headers:{'x-test':'response'}});
24        });
25        const url = `http://127.0.0.1:${server.addr.port}`;
26        const upload = new ReadableStream({async start(c) {
27            c.enqueue(new TextEncoder().encode('first'));
28            await new Promise(resolve => setTimeout(resolve,10));
29            c.enqueue(new TextEncoder().encode('second')); c.close();
30        }});
31        const pending = fetch(url+'/blocked', {method:'POST', headers:{'x-test':'request'}, body:upload, duplex:'half'});
32        if (await (await fetch(url+'/release')).text() !== 'released') throw Error('concurrency');
33        const response = await pending;
34        if (response.status !== 201 || response.headers.get('x-test') !== 'response' || await response.text() !== 'firstsecond') throw Error('streaming response');
35        await server.shutdown(); await server.finished;
36        console.log('streaming');
37        "#,
38        &[],
39    );
40    assert_eq!(output.stdout, b"streaming\n");
41}
42
43#[test]
44fn full_duplex_does_not_buffer_the_request_or_response() {
45    let output = common::run(
46        r#"
47        import {serve} from 'avril:std';
48        const server = serve({port:0}, request => new Response(request.body));
49        let upload;
50        const body = new ReadableStream({start(c) {upload=c;}});
51        const pending = fetch(`http://127.0.0.1:${server.addr.port}`, {method:'POST',body,duplex:'half'});
52        upload.enqueue(new Uint8Array([1,2,3]));
53        const response = await pending, reader = response.body.getReader();
54        const first = await reader.read();
55        if (first.done || first.value.join() !== '1,2,3') throw Error('first chunk');
56        upload.enqueue(new Uint8Array([4,5])); upload.close();
57        const second = await reader.read();
58        if (second.done || second.value.join() !== '4,5' || !(await reader.read()).done) throw Error('second chunk');
59        reader.releaseLock(); await server.shutdown(); console.log('duplex');
60        "#,
61        &[],
62    );
63    assert_eq!(output.stdout, b"duplex\n");
64}
65
66#[test]
67fn graceful_shutdown_waits_for_active_responses() {
68    let output = common::run(
69        r#"
70        import {serve} from 'avril:std';
71        let release, started;
72        const entered = new Promise(resolve => started=resolve);
73        const gate = new Promise(resolve => release=resolve);
74        const server = serve({port:0}, async request => {
75            started(); await gate;
76            if (request.signal.aborted) throw Error('shutdown aborted request');
77            return new Response('finished');
78        });
79        const pending = fetch(`http://127.0.0.1:${server.addr.port}`);
80        await entered;
81        let done = false;
82        const closing = server.shutdown(); closing.then(() => done=true);
83        if (server.shutdown() !== closing) throw Error('shutdown is not idempotent');
84        await new Promise(resolve => setTimeout(resolve,20));
85        if (done) throw Error('shutdown finished before response');
86        release(); if (await (await pending).text() !== 'finished') throw Error('lost response');
87        await closing;
88        try { await fetch(`http://127.0.0.1:${server.addr.port}`); throw Error('listener still open'); }
89        catch (error) { if (error.message === 'listener still open') throw error; }
90        console.log('shutdown');
91        "#,
92        &[],
93    );
94    assert_eq!(output.stdout, b"shutdown\n");
95}
96
97#[test]
98fn abort_closes_connections_and_signals_handlers() {
99    let output = common::run(
100        r#"
101        import {serve} from 'avril:std';
102        const control = new AbortController();
103        let entered, aborted;
104        const start = new Promise(resolve => entered=resolve);
105        const abort = new Promise(resolve => aborted=resolve);
106        const server = serve({port:0,signal:control.signal}, async request => {
107            request.signal.addEventListener('abort', () => aborted(request.signal.reason.name), {once:true});
108            entered(); await abort; request.signal.throwIfAborted(); return new Response('too late');
109        });
110        const pending = fetch(`http://127.0.0.1:${server.addr.port}`);
111        const result = pending.then(() => {throw Error('response after abort');}, () => {});
112        await start; control.abort();
113        if (await abort !== 'AbortError') throw Error('request signal');
114        await result; await server.finished; await server.shutdown();
115        console.log('abort');
116        "#,
117        &[],
118    );
119    assert_eq!(output.stdout, b"abort\n");
120    assert!(
121        output.stderr.is_empty(),
122        "{}",
123        String::from_utf8_lossy(&output.stderr)
124    );
125}
126
127#[test]
128fn handler_errors_invalid_responses_and_null_body_statuses() {
129    let output = common::run(
130        r#"
131        import {serve} from 'avril:std';
132        let canceled=false;
133        const server = serve({port:0}, request => {
134            const path = new URL(request.url).pathname;
135            if (path === '/throw') throw Error('broken');
136            if (path === '/invalid') return 'not a Response';
137            if (path === '/error') return Response.error();
138            if (path === '/used') { const r = new Response('used'); r.text(); return r; }
139            if (path === '/head') return new Response(new ReadableStream({cancel(){canceled=true;}}));
140            if (path === '/204') return new Response(null,{status:204});
141            if (path === '/205') return new Response(null,{status:205});
142            if (path === '/304') return new Response(null,{status:304});
143            return new Response('healthy');
144        });
145        const url=`http://127.0.0.1:${server.addr.port}`;
146        for (const path of ['/throw','/invalid','/error','/used']) {
147            const r=await fetch(url+path);
148            if (r.status!==500 || await r.text()!=='Internal Server Error') throw Error(path);
149        }
150        for (const status of [204,205,304]) {
151            const r=await fetch(url+'/'+status,{redirect:'manual'});
152            if (r.status!==status || r.body!==null) throw Error('null body');
153        }
154        const head=await fetch(url+'/head',{method:'HEAD'});
155        if (head.body!==null || !canceled) throw Error('HEAD');
156        if (await (await fetch(url)).text()!=='healthy') throw Error('server did not recover');
157        await server.shutdown();
158        const custom=serve({port:0,onError: error => new Response(error.message,{status:503})}, () => {throw Error('custom');});
159        const r=await fetch(`http://127.0.0.1:${custom.addr.port}`);
160        if (r.status!==503 || await r.text()!=='custom') throw Error('onError');
161        await custom.shutdown();
162        const broken=serve({port:0,onError: () => {throw Error('also broken');}}, () => {throw Error('broken');});
163        if ((await fetch(`http://127.0.0.1:${broken.addr.port}`)).status!==500) throw Error('onError fallback');
164        await broken.shutdown(); console.log('responses');
165        "#,
166        &[],
167    );
168    assert_eq!(output.stdout, b"responses\n");
169}
170
171#[test]
172fn listen_validation_permissions_and_callback_cleanup() {
173    let output = common::run(
174        r#"
175        import {serve,pledge} from 'avril:std';
176        for (const port of [-1,65536,1.5,NaN,Infinity]) {
177            try { serve({port}, () => new Response()); throw Error('accepted invalid port'); }
178            catch(e) {if (!(e instanceof RangeError)) throw e;}
179        }
180        for (const options of [{onError:1},{onListen:1},{signal:{}}]) {
181            try { serve(options, () => new Response()); throw Error('accepted invalid options'); }
182            catch(e) {if (!(e instanceof TypeError)) throw e;}
183        }
184        const stopped = new AbortController(); stopped.abort('already aborted');
185        try {serve({port:0,signal:stopped.signal}, () => new Response()); throw Error('already aborted');}
186        catch(e) {if(e!=='already aborted') throw e;}
187        let addr;
188        try {serve({port:0,onListen:a=>{addr=a;throw Error('onListen');}}, () => new Response());}
189        catch(e) {if(e.message!=='onListen') throw e;}
190        await new Promise(resolve => setTimeout(resolve,20));
191        const rebound=serve({port:addr.port}, () => new Response());
192        if (!Object.isFrozen(rebound.addr) || rebound.addr.hostname!=='127.0.0.1') throw Error('address');
193        await rebound.shutdown();
194        pledge({net:false});
195        try {serve({port:0}, () => new Response()); throw Error('missing permission');}
196        catch(e) {if(!e.message.includes('PermissionDenied')) throw e;}
197        console.log('permissions');
198        "#,
199        &[],
200    );
201    assert_eq!(output.stdout, b"permissions\n");
202}
203
204#[test]
205fn raw_http_keep_alive_preserves_host_headers_and_cookies() {
206    let (_dir, mut child) = common::spawn(
207        r#"
208        import {serve} from 'avril:std';
209        let count=0;
210        const server=serve({port:0,onListen:addr=>console.log(addr.port)}, request => {
211            if (request.url!=='http://example.test:4321/raw?q=1' || request.headers.get('host')!=='example.test:4321' || request.headers.get('x-test')!=='one, two') throw Error('wire request');
212            if (++count===2) server.shutdown();
213            return new Response('wire',{headers:[['set-cookie','a=1'],['set-cookie','b=2'],['x-latin','\u00e9']]});
214        });
215        await server.finished;
216        "#,
217    );
218    let mut stdout = BufReader::new(child.stdout.take().unwrap());
219    let mut line = String::new();
220    stdout.read_line(&mut line).unwrap();
221    let port: u16 = line.trim().parse().unwrap();
222    child.stdout = Some(stdout.into_inner());
223    let mut socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
224    socket
225        .set_read_timeout(Some(Duration::from_secs(3)))
226        .unwrap();
227    let request =
228        b"GET /raw?q=1 HTTP/1.1\r\nHost: example.test:4321\r\nX-Test: one\r\nX-Test: two\r\n\r\n";
229    let mut reader = BufReader::new(socket.try_clone().unwrap());
230    for _ in 0..2 {
231        socket.write_all(request).unwrap();
232        let mut response = Vec::new();
233        loop {
234            let mut line = Vec::new();
235            reader.read_until(b'\n', &mut line).unwrap();
236            assert!(!line.is_empty(), "unexpected EOF");
237            response.extend_from_slice(&line);
238            if line == b"\r\n" {
239                break;
240            }
241        }
242        let headers = String::from_utf8_lossy(&response).to_ascii_lowercase();
243        assert!(headers.starts_with("http/1.1 200 ok\r\n"), "{headers}");
244        assert!(headers.contains("set-cookie: a=1\r\n"));
245        assert!(headers.contains("set-cookie: b=2\r\n"));
246        assert!(
247            response
248                .windows(12)
249                .any(|bytes| bytes == b"x-latin: \xe9\r\n")
250        );
251        let mut body = [0; 4];
252        reader.read_exact(&mut body).unwrap();
253        assert_eq!(&body, b"wire");
254    }
255    let output = common::wait(child);
256    assert!(
257        output.status.success(),
258        "{}",
259        String::from_utf8_lossy(&output.stderr)
260    );
261}
262
263#[test]
264fn request_signal_follows_client_disconnect() {
265    let output = common::run(
266        r#"
267        import {serve} from 'avril:std';
268        let entered, disconnected;
269        const start=new Promise(resolve=>entered=resolve);
270        const gone=new Promise(resolve=>disconnected=resolve);
271        const server=serve({port:0}, async request=>{
272            request.signal.addEventListener('abort',disconnected,{once:true});
273            entered(); await gone; request.signal.throwIfAborted(); return new Response('late');
274        });
275        const client=new AbortController();
276        const pending=fetch(`http://127.0.0.1:${server.addr.port}`,{signal:client.signal});
277        const settled=pending.catch(()=>{});
278        await start; client.abort(); await gone; await settled;
279        await server.shutdown(); console.log('disconnect');
280        "#,
281        &[],
282    );
283    assert_eq!(output.stdout, b"disconnect\n");
284    assert!(
285        output.stderr.is_empty(),
286        "{}",
287        String::from_utf8_lossy(&output.stderr)
288    );
289}
290
291#[test]
292fn server_in_worker_uses_worker_capabilities() {
293    let dir = tempfile::tempdir().unwrap();
294    let path = dir.path().join("worker.js");
295    std::fs::write(
296        &path,
297        r#"
298        import {serve} from 'avril:std';
299        try {serve({port:0},()=>new Response()); postMessage('escaped');}
300        catch(e) {postMessage(e.message);}
301        close();
302    "#,
303    )
304    .unwrap();
305    let url = url::Url::from_file_path(path).unwrap();
306    let output = common::run(
307        &format!(
308            r#"
309        import {{avril}} from 'avril:std';
310        const worker=new Worker('{url}',{{type:'module',[avril]:{{caps:{{net:false}}}}}});
311        const result=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data));
312        if(!result.includes('PermissionDenied')) throw Error(result);
313        worker.terminate(); console.log('worker');
314        "#
315        ),
316        &[],
317    );
318    assert_eq!(output.stdout, b"worker\n");
319}
320
321#[test]
322fn handler_failures_report_uncaught_stacks_without_stopping_the_server() {
323    let output = common::run(
324        r#"
325        import {serve} from 'avril:std';
326        const errors=[],stacks=[];
327        addEventListener('error',event=>{
328            if(event.error!==errors[stacks.length]||event.message!==event.error.message||!event.cancelable)throw Error('error event');
329            stacks.push(event.error.stack);
330        });
331        console.error=()=>{throw Error('guest console');};
332        function throwingHandler(request) {
333            const path=new URL(request.url).pathname;
334            if(path==='/healthy')return new Response('healthy');
335            const error=new Error(path==='/throw'?'handler failed':'handler rejected');
336            errors.push(error);
337            if(path==='/throw')throw error;
338            return Promise.reject(error);
339        }
340        const server=serve({port:0},throwingHandler),url=`http://127.0.0.1:${server.addr.port}`;
341        for(const path of ['/throw','/reject']) {
342            const response=await fetch(url+path);
343            if(response.status!==500||await response.text()!=='Internal Server Error')throw Error('fallback');
344            if(await (await fetch(url+'/healthy')).text()!=='healthy')throw Error('server stopped');
345        }
346        if(stacks.length!==2)throw Error('duplicate or missing reports');
347        await server.shutdown();console.log(JSON.stringify(stacks));
348    "#,
349        &[],
350    );
351    let stacks: Vec<String> = serde_json::from_slice(&output.stdout).unwrap();
352    assert!(stacks[0].starts_with("Error: handler failed\n    at throwingHandler"));
353    assert!(stacks[1].starts_with("Error: handler rejected\n    at throwingHandler"));
354    assert_eq!(
355        String::from_utf8(output.stderr).unwrap(),
356        format!("{}\n", stacks.join("\n"))
357    );
358}
359
360#[test]
361fn canceled_http_error_events_suppress_stderr_and_on_error_handles_original_failures() {
362    let output = common::run(
363        r#"
364        import {serve} from 'avril:std';
365        const errors=[],failure=Error('handler failure');
366        addEventListener('error',event=>{errors.push(event.error);event.preventDefault();});
367        const server=serve({port:0},async request=>{
368            const path=new URL(request.url).pathname;
369            if(path==='/throw')throw failure;
370            if(path==='/invalid')return null;
371            const used=new Response('used');await used.text();return used;
372        });
373        for(const path of ['/throw','/invalid','/used']) {
374            const response=await fetch(`http://127.0.0.1:${server.addr.port}${path}`);
375            if(response.status!==500||await response.text()!=='Internal Server Error')throw Error('fallback');
376        }
377        if(errors.length!==3||errors[0]!==failure||!(errors[1] instanceof TypeError)||!(errors[2] instanceof TypeError))throw Error('handler reports');
378        await server.shutdown();
379        const recovered=serve({port:0,onError:error=>new Response(error.message,{status:503})},()=>{throw failure;});
380        const custom=await fetch(`http://127.0.0.1:${recovered.addr.port}`);
381        if(custom.status!==503||await custom.text()!==failure.message||errors.length!==3)throw Error('handled failure reported');
382        await recovered.shutdown();
383        for(const onError of [()=>{throw failure;},()=>null]) {
384            const broken=serve({port:0,onError},()=>{throw failure;});
385            const response=await fetch(`http://127.0.0.1:${broken.addr.port}`);
386            if(response.status!==500||await response.text()!=='Internal Server Error')throw Error('onError fallback');
387            await broken.shutdown();
388        }
389        if(errors.length!==5||errors[3]!==failure||!(errors[4] instanceof TypeError))throw Error('onError reports');
390        console.log('canceled');
391    "#,
392        &[],
393    );
394    assert_eq!(output.stdout, b"canceled\n");
395    assert!(
396        output.stderr.is_empty(),
397        "{}",
398        String::from_utf8_lossy(&output.stderr)
399    );
400}
401
402#[test]
403fn on_error_and_response_stream_failures_are_reported_once_nonfatally() {
404    let output = common::run(
405        r#"
406        import {serve} from 'avril:std';
407        const reports=[];
408        addEventListener('error',event=>reports.push(event.message));
409        const broken=serve({port:0,onError:()=>{throw Error('onError failed');}},()=>{throw Error('handled original');});
410        const fallback=await fetch(`http://127.0.0.1:${broken.addr.port}`);
411        if(fallback.status!==500||await fallback.text()!=='Internal Server Error')throw Error('onError fallback');
412        await broken.shutdown();
413        let fail;
414        const server=serve({port:0},request=>{
415            if(new URL(request.url).pathname==='/healthy')return new Response('healthy');
416            const gate=new Promise(resolve=>fail=resolve);
417            return new Response(new ReadableStream({async start(c){
418                c.enqueue(new Uint8Array([7]));await gate;c.error(Error('response stream failed'));
419            }}));
420        });
421        const url=`http://127.0.0.1:${server.addr.port}`,response=await fetch(url),reader=response.body.getReader();
422        if((await reader.read()).value[0]!==7)throw Error('first chunk');
423        fail();
424        try{await reader.read();throw Error('stream succeeded');}
425        catch(error){if(error.message==='stream succeeded')throw error;}
426        reader.releaseLock();
427        if(await (await fetch(url+'/healthy')).text()!=='healthy')throw Error('server stopped');
428        await server.shutdown();
429        if(reports.join('|')!=='onError failed|response stream failed')throw Error('reports');
430        console.log('reported');
431    "#,
432        &[],
433    );
434    assert_eq!(output.stdout, b"reported\n");
435    let stderr = String::from_utf8(output.stderr).unwrap();
436    assert_eq!(
437        stderr.matches("Error: onError failed\n").count(),
438        1,
439        "{stderr}"
440    );
441    assert_eq!(
442        stderr.matches("Error: response stream failed\n").count(),
443        1,
444        "{stderr}"
445    );
446    assert!(stderr.contains("\n    at "), "{stderr}");
447    assert!(!stderr.contains("handled original"), "{stderr}");
448}
449
450#[test]
451fn hostile_stack_getters_do_not_turn_http_reporting_into_a_failure() {
452    let output = common::run(
453        r#"
454        import {serve} from 'avril:std';
455        const failure=Error('original failure');
456        Object.defineProperty(failure,'stack',{get(){throw Error('stack getter failed');}});
457        const server=serve({port:0},request=>{
458            if(new URL(request.url).pathname==='/healthy')return new Response('healthy');
459            throw failure;
460        });
461        const url=`http://127.0.0.1:${server.addr.port}`,response=await fetch(url);
462        if(response.status!==500||await response.text()!=='Internal Server Error')throw Error('fallback');
463        if(await (await fetch(url+'/healthy')).text()!=='healthy')throw Error('server stopped');
464        await server.shutdown();console.log('safe reporting');
465    "#,
466        &[],
467    );
468    assert_eq!(output.stdout, b"safe reporting\n");
469    assert_eq!(output.stderr, b"Error: original failure\n");
470}
471
472#[test]
473fn option_conversion_callback_receivers_and_frozen_server_metadata() {
474    let output = common::run(
475        r#"
476        import {serve, serveFile, upgradeWebSocket} from 'avril:std';
477        if (serve.length !== 1 || serveFile.length !== 2 || upgradeWebSocket.length !== 1) throw Error('arity');
478        const order = [], failure = {};
479        let hostnameReads = 0, portReads = 0, listened;
480        const options = {
481            get hostname() {
482                order.push('hostname');
483                if (++hostnameReads === 1) return Symbol('presence only');
484                return {toString() {order.push('hostname:string'); return '127.0.0.1';}};
485            },
486            get port() {
487                order.push('port');
488                if (++portReads === 1) return Symbol('presence only');
489                return {valueOf() {order.push('port:number'); return 0;}};
490            },
491            get signal() {order.push('signal'); return undefined;},
492            get onError() {
493                order.push('onError');
494                return function(error) {
495                    if (this !== undefined || error !== failure) throw Error('onError receiver or reason');
496                    order.push('recover');
497                    return {then(resolve) {order.push('recover:then'); resolve(new Response('recovered', {status:202}));}};
498                };
499            },
500            get onListen() {
501                order.push('onListen');
502                return function(addr) {
503                    if (this !== undefined) throw Error('onListen receiver');
504                    order.push('listened'); listened = addr;
505                };
506            },
507            get path() {order.push('path'); return undefined;}
508        };
509        const server = serve(options, function(request, info) {
510            if (this !== undefined || request.method !== 'GET') throw Error('handler receiver');
511            for (const value of [info, info.remoteAddr]) {
512                if (!Object.isFrozen(value) || Object.getPrototypeOf(value) !== null) throw Error('mutable metadata');
513            }
514            if (info.remoteAddr.transport !== 'tcp' || info.remoteAddr.hostname !== '127.0.0.1') throw Error('peer');
515            order.push('handler');
516            return {then(resolve, reject) {order.push('handler:then'); reject(failure);}};
517        });
518        const prefix = 'hostname|hostname|hostname:string|port|port|port:number|signal|onError|onListen|path|listened';
519        if (order.join('|') !== prefix) throw Error('conversion order: '+order);
520        if (server.addr !== listened) throw Error('listen address identity');
521        for (const value of [server, server.addr]) {
522            if (!Object.isFrozen(value) || Object.getPrototypeOf(value) !== null) throw Error('mutable server');
523        }
524        const response = await fetch(`http://127.0.0.1:${server.addr.port}`);
525        if (response.status !== 202 || await response.text() !== 'recovered') throw Error('thenable recovery');
526        if (order.join('|') !== prefix+'|handler|handler:then|recover|recover:then') throw Error('callback order');
527        if (server.shutdown() !== server.finished) throw Error('completion identity');
528        await server.finished;
529        console.log('server shape and conversion');
530        "#,
531        &[],
532    );
533    assert_eq!(output.stdout, b"server shape and conversion\n");
534    assert!(
535        output.stderr.is_empty(),
536        "{}",
537        String::from_utf8_lossy(&output.stderr)
538    );
539}