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