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