|
| 1 | +// Covers two pieces of the fetch inspector instrumentation that the |
| 2 | +// upstream node_compat test doesn't exercise: |
| 3 | +// |
| 4 | +// 1. Redirect chain - a 302 hop should keep the same requestId and the |
| 5 | +// next `requestWillBeSent` should carry a `redirectResponse` of the |
| 6 | +// previous hop. The intermediate 30x should NOT fire its own |
| 7 | +// `responseReceived`. |
| 8 | +// 2. Streaming request body - when fetch's body is a ReadableStream, |
| 9 | +// `Network.getRequestPostData` must reject with "not finished yet" |
| 10 | +// because we never emit `dataSent({finished:true})` for that path. |
| 11 | +import inspector from "node:inspector/promises"; |
| 12 | +import { strict as assert } from "node:assert"; |
| 13 | + |
| 14 | +const session = new inspector.Session(); |
| 15 | +session.connect(); |
| 16 | +await session.post("Network.enable"); |
| 17 | + |
| 18 | +// ---- Test 1: redirect chain ------------------------------------------------ |
| 19 | +{ |
| 20 | + const server = Deno.serve({ port: 0, onListen: () => {} }, (req) => { |
| 21 | + const url = new URL(req.url); |
| 22 | + if (url.pathname === "/start") { |
| 23 | + return new Response(null, { |
| 24 | + status: 302, |
| 25 | + headers: { location: "/landing" }, |
| 26 | + }); |
| 27 | + } |
| 28 | + return new Response("ok", { headers: { "content-type": "text/plain" } }); |
| 29 | + }); |
| 30 | + |
| 31 | + const events = []; |
| 32 | + const onEvent = ({ method, params }) => events.push({ method, params }); |
| 33 | + session.on("Network.requestWillBeSent", onEvent); |
| 34 | + session.on("Network.responseReceived", onEvent); |
| 35 | + session.on("Network.loadingFinished", onEvent); |
| 36 | + |
| 37 | + const startUrl = `http://127.0.0.1:${server.addr.port}/start`; |
| 38 | + const finalUrl = `http://127.0.0.1:${server.addr.port}/landing`; |
| 39 | + const resp = await fetch(startUrl); |
| 40 | + await resp.text(); |
| 41 | + // Let the background drain emit loadingFinished. |
| 42 | + await new Promise((r) => setTimeout(r, 50)); |
| 43 | + |
| 44 | + session.off("Network.requestWillBeSent", onEvent); |
| 45 | + session.off("Network.responseReceived", onEvent); |
| 46 | + session.off("Network.loadingFinished", onEvent); |
| 47 | + await server.shutdown(); |
| 48 | + |
| 49 | + const willBeSent = events.filter((e) => |
| 50 | + e.method === "Network.requestWillBeSent" |
| 51 | + ); |
| 52 | + const received = events.filter((e) => |
| 53 | + e.method === "Network.responseReceived" |
| 54 | + ); |
| 55 | + const finished = events.filter((e) => e.method === "Network.loadingFinished"); |
| 56 | + |
| 57 | + assert.equal(willBeSent.length, 2, "expected two requestWillBeSent events"); |
| 58 | + assert.equal( |
| 59 | + willBeSent[0].params.requestId, |
| 60 | + willBeSent[1].params.requestId, |
| 61 | + "redirect hop should reuse the original requestId", |
| 62 | + ); |
| 63 | + assert.equal(willBeSent[0].params.request.url, startUrl); |
| 64 | + assert.equal(willBeSent[0].params.redirectResponse, undefined); |
| 65 | + assert.equal(willBeSent[1].params.request.url, finalUrl); |
| 66 | + assert.equal(willBeSent[1].params.redirectResponse.status, 302); |
| 67 | + assert.equal(willBeSent[1].params.redirectResponse.url, startUrl); |
| 68 | + |
| 69 | + // The intermediate 302 must not produce its own responseReceived; only |
| 70 | + // the final 200 should. |
| 71 | + assert.equal(received.length, 1, "exactly one responseReceived"); |
| 72 | + assert.equal(received[0].params.response.status, 200); |
| 73 | + assert.equal(received[0].params.response.url, finalUrl); |
| 74 | + |
| 75 | + assert.equal(finished.length, 1, "exactly one loadingFinished"); |
| 76 | + assert.equal( |
| 77 | + finished[0].params.requestId, |
| 78 | + willBeSent[0].params.requestId, |
| 79 | + "loadingFinished should share the chain's requestId", |
| 80 | + ); |
| 81 | + console.log("PASS: redirect chain emits with shared requestId"); |
| 82 | +} |
| 83 | + |
| 84 | +// ---- Test 2: streaming request body --------------------------------------- |
| 85 | +{ |
| 86 | + const server = Deno.serve( |
| 87 | + { port: 0, onListen: () => {} }, |
| 88 | + async (req) => new Response(await req.text()), |
| 89 | + ); |
| 90 | + |
| 91 | + let requestId; |
| 92 | + const gotRequestWillBeSent = new Promise((resolve) => { |
| 93 | + session.once("Network.requestWillBeSent", ({ params }) => { |
| 94 | + requestId = params.requestId; |
| 95 | + resolve(); |
| 96 | + }); |
| 97 | + }); |
| 98 | + |
| 99 | + const body = new ReadableStream({ |
| 100 | + start(c) { |
| 101 | + c.enqueue(new TextEncoder().encode("streamed-body")); |
| 102 | + c.close(); |
| 103 | + }, |
| 104 | + }); |
| 105 | + const resp = await fetch(`http://127.0.0.1:${server.addr.port}/`, { |
| 106 | + method: "POST", |
| 107 | + body, |
| 108 | + }); |
| 109 | + await resp.text(); |
| 110 | + await gotRequestWillBeSent; |
| 111 | + |
| 112 | + // For streaming bodies we deliberately don't flip is_request_finished - |
| 113 | + // there's no chunked dataSent path wired yet, so getRequestPostData |
| 114 | + // must reject rather than return garbage. |
| 115 | + let rejected = false; |
| 116 | + try { |
| 117 | + await session.post("Network.getRequestPostData", { requestId }); |
| 118 | + } catch (err) { |
| 119 | + rejected = true; |
| 120 | + assert.match( |
| 121 | + String(err.message ?? err), |
| 122 | + /not finished yet/i, |
| 123 | + "should reject with 'not finished yet'", |
| 124 | + ); |
| 125 | + } |
| 126 | + assert.equal( |
| 127 | + rejected, |
| 128 | + true, |
| 129 | + "getRequestPostData should reject for streaming bodies", |
| 130 | + ); |
| 131 | + |
| 132 | + await server.shutdown(); |
| 133 | + console.log("PASS: streaming-body request stays un-finished"); |
| 134 | +} |
| 135 | + |
| 136 | +session.disconnect(); |
| 137 | +console.log("ALL PASSED"); |
0 commit comments