mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
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:
1 parent
a97f827469
commit
d5ba7cec60
2 files changed
+119
-2
No files matched your search
@@ -0,0 +1,84 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
|
||||
/**
|
||||
* 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 exposes `json`/`text`/`arrayBuffer`, and an un-awaited
|
||||
* `fetch` returns a `Promise`. In every one of those the call throws at the call
|
||||
* site instead of reading a body, so it is not a response consumer.
|
||||
*/
|
||||
const consumersFor = (source: string) =>
|
||||
analyzeJavaScriptSemantics(source)
|
||||
.requestOperations.filter(({ kind }) => kind === "response-consumer")
|
||||
.map(({ method, endpoint, resolution }) => ({
|
||||
method,
|
||||
endpoint,
|
||||
resolution,
|
||||
}));
|
||||
|
||||
it.each([
|
||||
[
|
||||
"an un-awaited fetch",
|
||||
`async function f(){ const res = fetch("https://api.test/x"); return res.json(); }`,
|
||||
],
|
||||
[
|
||||
"an http.get request",
|
||||
`import http from "node:http";
|
||||
function f(){ const req = http.get("https://api.test/x"); return req.json(); }`,
|
||||
],
|
||||
[
|
||||
"an http.request request",
|
||||
`import http from "node:http";
|
||||
function f(){ const req = http.request("https://api.test/x"); return req.text(); }`,
|
||||
],
|
||||
[
|
||||
"a WebSocket handshake",
|
||||
`function f(){ const ws = new WebSocket("wss://api.test/x"); return ws.json(); }`,
|
||||
],
|
||||
])("emits no response consumer for %s", (_name, source) => {
|
||||
expect(consumersFor(source)).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["json", "json"],
|
||||
["text", "text"],
|
||||
["arrayBuffer", "arrayBuffer"],
|
||||
] as const)("keeps the settled fetch body reader for %s", (_name, method) => {
|
||||
expect(
|
||||
consumersFor(
|
||||
`async function f(){ const res = await fetch("https://api.test/x"); return res.${method}(); }`,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
method,
|
||||
endpoint: "https://api.test/x",
|
||||
resolution: "complete",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a settled fetch reader through a wrapper expression", () => {
|
||||
expect(
|
||||
consumersFor(
|
||||
`async function f(){ const res = await fetch("https://api.test/x"); return (res).json(); }`,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps the fetch candidate when the binding also holds another request", () => {
|
||||
// The mixed binding keeps its fetch link, so the consumer stays a candidate
|
||||
// rather than losing the endpoint entirely.
|
||||
const consumers = consumersFor(`
|
||||
import http from "node:http";
|
||||
async function f(){
|
||||
let res = await fetch("https://api.test/x");
|
||||
res = await http.get("https://api.test/y");
|
||||
return res.json();
|
||||
}
|
||||
`);
|
||||
expect(consumers).toEqual([
|
||||
{ method: "json", endpoint: null, resolution: "partial" },
|
||||
]);
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user