mod common; #[test] fn revoking_network_authority_prevents_a_pending_response() { let output = common::run( r#" import {serve,pledge} from 'avril:std'; const server=serve({port:0}, () => { pledge({net:false}); return new Response('must not escape'); }); try { await fetch(`http://127.0.0.1:${server.addr.port}`); throw Error('authority escaped'); } catch(error) {if(error.message==='authority escaped') throw error;} await server.shutdown(); console.log('revoked'); "#, &[], ); assert_eq!(output.stdout, b"revoked\n"); } #[test] fn canceling_a_response_cancels_a_stalled_source_despite_cleanup_errors() { let output = common::run( r#" import {serve} from 'avril:std'; let canceled, disconnected; const cancel=new Promise(resolve=>canceled=resolve); const gone=new Promise(resolve=>disconnected=resolve); const server=serve({port:0}, request=>{ request.signal.addEventListener('abort',disconnected,{once:true}); return new Response(new ReadableStream({ start(c){c.enqueue(new Uint8Array([7]));}, pull(){return new Promise(()=>{});}, cancel(){canceled(); return Promise.reject(Error('cleanup failed'));} })); }); const response=await fetch(`http://127.0.0.1:${server.addr.port}`); const reader=response.body.getReader(); if((await reader.read()).value[0]!==7) throw Error('first chunk'); await reader.cancel(); reader.releaseLock(); await cancel; await gone; await server.shutdown(); console.log('canceled'); "#, &[], ); assert_eq!(output.stdout, b"canceled\n"); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn invalid_stream_chunks_close_only_the_affected_connection() { let output = common::run( r#" import {serve} from 'avril:std'; let badChunk; const reports=[]; addEventListener('error',event=>reports.push(event.error)); const ready=new Promise(resolve=>badChunk=resolve); const server=serve({port:0}, request=>{ if(new URL(request.url).pathname==='/healthy') return new Response('healthy'); return new Response(new ReadableStream({async start(c){ c.enqueue(new Uint8Array([1])); await ready; c.enqueue('not bytes'); c.close(); }})); }); const url=`http://127.0.0.1:${server.addr.port}`; const response=await fetch(url), reader=response.body.getReader(); if((await reader.read()).value[0]!==1) throw Error('first chunk'); badChunk(); try {await reader.read(); throw Error('accepted bad chunk');} catch(error) {if(error.message==='accepted bad chunk') throw error;} reader.releaseLock(); if(await (await fetch(url+'/healthy')).text()!=='healthy') throw Error('server stopped'); await server.shutdown(); if(reports.length!==1||!(reports[0] instanceof TypeError))throw Error('stream reports'); console.log('bad chunk'); "#, &[], ); assert_eq!(output.stdout, b"bad chunk\n"); let stderr = String::from_utf8(output.stderr).unwrap(); assert_eq!( stderr .matches("TypeError: Response chunks must be Uint8Array\n") .count(), 1, "{stderr}" ); } #[test] fn request_body_cancellation_does_not_cancel_the_response() { let output = common::run( r#" import {serve} from 'avril:std'; const server=serve({port:0},async request=>{ await request.body.cancel('not needed'); if(request.signal.aborted) throw Error('body cancel aborted request'); return new Response('accepted'); }); const response=await fetch(`http://127.0.0.1:${server.addr.port}`,{method:'POST',body:new Uint8Array(65536)}); if(await response.text()!=='accepted') throw Error('response canceled'); await server.shutdown(); console.log('body cancel'); "#, &[], ); assert_eq!(output.stdout, b"body cancel\n"); } #[test] fn serving_uses_captured_intrinsics_not_guest_promise_hooks() { let output = common::run( r#" import {serve} from 'avril:std'; const encode=new TextEncoder(), chunk=encode.encode('safe'); const requestText=Request.prototype.text; const originalThen=Promise.prototype.then, originalPromise=Promise; function observe(promise) { Object.defineProperty(promise,'constructor',{value:undefined}); const result=Reflect.apply(originalThen,promise,[value=>value,error=>{throw error;}]); Object.defineProperty(result,'constructor',{value:originalPromise}); return result; } const poison=()=>{throw Error('guest hook');}; Object.defineProperty(Promise.prototype,'constructor',{get:poison,configurable:true}); Object.defineProperty(Promise,Symbol.species,{get:poison}); Promise.prototype.then=poison; Array.prototype[Symbol.iterator]=poison; Request.prototype.text=poison; Object.defineProperty(Response.prototype,'status',{get:poison}); const server=serve({port:0},async request=>{ if(request.method!=='POST') throw Error('method'); if(await observe(Reflect.apply(requestText,request,[]))!=='safe') throw Error('request body'); return new Response(new ReadableStream({start(c){c.enqueue(chunk);c.close();}})); }); const response=await observe(fetch(`http://127.0.0.1:${server.addr.port}`,{method:'POST',body:chunk})); if(await observe(response.text())!=='safe') throw Error('response body'); await observe(server.shutdown()); console.log('intrinsics'); "#, &[], ); assert_eq!(output.stdout, b"intrinsics\n"); } #[test] fn content_length_completion_does_not_report_a_disconnect() { let output = common::run( r#" import {serve} from 'avril:std'; let signal,canceled; const sourceCanceled=new Promise(resolve=>canceled=resolve); const server=serve({port:0},request=>{ signal=request.signal; return new Response(new ReadableStream({ start(c){c.enqueue(new TextEncoder().encode('exact'));}, cancel(){canceled();} }),{headers:{'content-length':'5'}}); }); const response=await fetch(`http://127.0.0.1:${server.addr.port}`); if(await response.text()!=='exact') throw Error('content length'); await sourceCanceled; if(signal.aborted) throw Error('normal completion aborted request'); await server.shutdown(); console.log('length'); "#, &[], ); assert_eq!(output.stdout, b"length\n"); } #[test] fn response_writes_retain_native_buffer_checks_and_release_the_reader() { let output = common::run( r#" import {serve} from 'avril:std'; const reports = []; addEventListener('error', event => {reports.push(event.error); event.preventDefault();}); for (const [chunk, expected] of [ [new Uint8Array(new SharedArrayBuffer(1)), 'buffer'], [new Uint8Array(new ArrayBuffer(1, {maxByteLength:4})), 'resizable'] ]) { let releaseChunk, canceled, body; const gate = new Promise(resolve => releaseChunk = resolve); const cleanup = new Promise(resolve => canceled = resolve); const server = serve({port:0}, request => { if (new URL(request.url).pathname === '/healthy') return new Response('healthy'); body = new ReadableStream({ async start(controller) { controller.enqueue(new Uint8Array([7])); await gate; controller.enqueue(chunk); }, cancel() {canceled();} }); return new Response(body); }); const url = `http://127.0.0.1:${server.addr.port}`; const response = await fetch(url), reader = response.body.getReader(); if ((await reader.read()).value[0] !== 7) throw Error('first chunk'); releaseChunk(); try {await reader.read(); throw Error('accepted unsupported buffer');} catch (error) {if (error.message === 'accepted unsupported buffer') throw error;} reader.releaseLock(); await cleanup; if (body.locked) throw Error('response reader retained'); const error = reports.pop(); if (!(error instanceof TypeError) || !error.message.toLowerCase().includes(expected) || reports.length) throw Error('native write report'); if (await (await fetch(url+'/healthy')).text() !== 'healthy') throw Error('server stopped'); await server.shutdown(); } console.log('native writes and cleanup'); "#, &[], ); assert_eq!(output.stdout, b"native writes and cleanup\n"); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn invalid_framing_headers_fail_before_sending_a_response() { let output = common::run( r#" import {serve} from 'avril:std'; const server=serve({port:0},request=>{ const path=new URL(request.url).pathname; if(path==='/length') return new Response('short',{headers:{'content-length':'100'}}); if(path==='/invalid') return new Response('short',{headers:{'content-length':'5, 5'}}); return new Response('short',{headers:{'transfer-encoding':'chunked'}}); }); const url=`http://127.0.0.1:${server.addr.port}`; for(const path of ['/length','/invalid','/encoding']) { const response=await fetch(url+path); if(response.status!==500 || await response.text()!=='Internal Server Error') throw Error(path); } await server.shutdown(); console.log('framing'); "#, &[], ); assert_eq!(output.stdout, b"framing\n"); } #[test] fn terminating_a_worker_releases_its_listener() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("worker.js"); std::fs::write( &path, r#" import {serve} from 'avril:std'; const server=serve({port:0},()=>new Response('worker')); postMessage(server.addr.port); await server.finished; "#, ) .unwrap(); let url = url::Url::from_file_path(path).unwrap(); let output = common::run( &format!( r#" import {{serve}} from 'avril:std'; const worker=new Worker('{url}',{{type:'module'}}); const port=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data)); if(await (await fetch(`http://127.0.0.1:${{port}}`)).text()!=='worker') throw Error('worker listener'); worker.terminate(); let rebound; for(let i=0;i<100&&!rebound;i++) {{ try {{rebound=serve({{port}},()=>new Response('parent'));}} catch {{await new Promise(resolve=>setTimeout(resolve,5));}} }} if(!rebound) throw Error('worker leaked listener'); await rebound.shutdown(); console.log('terminated'); "# ), &[], ); assert_eq!(output.stdout, b"terminated\n"); }