fix(javascript): link a response body reader only to a settled fetch (#8028)

* fix(javascript): link a response body reader only to a settled fetch

`collectResponseConsumers` treated any `.json()`/`.text()`/`.arrayBuffer()` on a
binding holding a request result as a response consumer, and gave it the
request's endpoint at `resolution: "complete"`. That holds only for a settled
`fetch`.

Measured on Node 24.22.1:

    await fetch(u)               -> Response      json/text/arrayBuffer = function
    fetch(u) unawaited           -> Promise       json = undefined; pending.json() THROWS TypeError
    http.get(u)                  -> ClientRequest json/text/arrayBuffer = undefined
    http.request(u)              -> ClientRequest json = undefined; request.text() THROWS TypeError
    new WebSocket(u)             -> WebSocket     socket.json() THROWS TypeError
    await http.get(u)            -> ClientRequest (await does not help)

So four shapes emitted a resolved "this endpoint's body is read here" fact for a
call that throws before any body is read: an un-awaited fetch, `http.get`,
`http.request`, and a `WebSocket` handshake.

Link only requests that settle to a `Response`: a `fetch` call whose value is
awaited through expression wrappers. A binding that also holds a request kind
without a body reader keeps the consumer a candidate (`partial`) rather than a
proven one, and a binding with no settled fetch emits no consumer at all, which
matches the `collectReleases` / `forkRejectsStdioArray` precedent — the receiver
provably has no such member, so there is no fact to report.

* chore: retrigger title and readiness checks after the title was corrected
This commit is contained in:
吴佳浩Alben authored and GitHub committed 2026-11-04 19:35:16 +08:00
1 parent a97f827469
commit d5ba7cec60
2 files changed
+119 -2

No files matched your search

@@ -8,6 +8,7 @@ import type {
} from "./javascriptSemanticIr.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
javaScriptExpressionWrapperTarget,
readExactJavaScriptLiteral,
semanticStaticPropertyKey,
semanticStaticPropertyName,
@@ -304,6 +305,28 @@ const soleWriteReachesRead = (
);
};
/**
* Only `fetch` resolves to a `Response`, and only once its promise settles.
* `http.request`/`http.get` return a `ClientRequest` and `new WebSocket` a
* socket, neither of which has `json`/`text`/`arrayBuffer`; an un-awaited
* `fetch` returns a `Promise`. In all of those the call throws at the call site
* instead of reading a body, so it is not a response consumer at all.
*/
const settlesToResponse = (
request: RequestCandidate,
context: TraversalContext,
): boolean => {
if (request.operation.method !== "fetch") return false;
let node: t.Node = request.node;
for (;;) {
const parent = context.state.parentsByNode.get(node);
if (parent === undefined) return false;
if (t.isAwaitExpression(parent)) return true;
if (javaScriptExpressionWrapperTarget(parent) !== node) return false;
node = parent;
}
};
const collectResponseConsumers = (
program: t.Program,
context: TraversalContext,
@@ -338,11 +361,20 @@ const collectResponseConsumers = (
member.object,
member.object.name,
);
const linked =
const candidates =
binding === undefined
? []
: (requestsByBinding.get(binding.bindingId) ?? []);
// A body reader only exists on a settled fetch Response. Requests of any
// other kind throw at this call site instead, so they link nothing; if no
// candidate survives, the call is not a response consumer at all.
const linked = candidates.filter((request) =>
settlesToResponse(request, context),
);
if (linked.length === 0) return;
// A binding that also receives a request kind without a body reader leaves
// this reader a candidate, not a proven consumer of the fetch response.
const otherKinds = candidates.length !== linked.length;
// A consumer of exactly one request through its own result binding
// reads that request's response, so it keeps the request's endpoint.
const uniqueRequest =
@@ -377,7 +409,8 @@ const collectResponseConsumers = (
.sort(compareUnicodeCodePoints),
endpoint: soleWrite ? (uniqueRequest?.operation.endpoint ?? null) : null,
fields: [],
resolution: uniqueRequest === undefined ? "partial" : "complete",
resolution:
uniqueRequest === undefined || otherKinds ? "partial" : "complete",
});
});
return output;