fix(js): resolve literal URL from Request constructor in fetch calls (#6364)

* fix(js): resolve literal URL from Request constructor in fetch calls

* fix(js): gate Request URL extraction on fetch and require new operator
This commit is contained in:
Ahmed authored and GitHub committed 2026-10-23 05:38:17 +08:00
1 parent ee0ec027db
commit 3984d12a30
2 files changed
+153 -8

No files matched your search

@@ -263,3 +263,114 @@ it("keeps exact literal targets exact", () => {
},
]);
});
it("resolves inline Request constructor targets with literal URLs", () => {
expect(
endpoints(`
fetch(new Request("/api/chat", { method: "POST" }));
window.fetch(new Request("/api/window"));
`),
).toEqual([
{
kind: "network",
value: "/api/chat",
mechanism: "call:fetch",
resolution: undefined,
},
{
kind: "network",
value: "/api/window",
mechanism: "call:window.fetch",
resolution: undefined,
},
]);
});
it("keeps Request calls without new partial", () => {
expect(endpoints('fetch(Request("/api/stream"));')).toEqual([
{
kind: "network",
value: 'Request("/api/stream")',
mechanism: "call:fetch",
resolution: "partial",
},
]);
});
it("resolves unchanged Request objects and aliases", () => {
expect(
endpoints(`
const req = new Request("/api/chat", { method: "POST" });
const alias = req;
fetch(alias);
`),
).toEqual([
{
kind: "network",
value: "/api/chat",
mechanism: "call:fetch",
resolution: undefined,
},
]);
});
it("keeps Request objects partial when called with toString", () => {
expect(
endpoints(`
const req = new Request("/api/chat");
fetch(req.toString());
`),
).toEqual([
{
kind: "network",
value: "req.toString()",
mechanism: "call:fetch",
resolution: "partial",
},
]);
});
it("keeps Request targets partial on non-fetch network APIs", () => {
expect(
endpoints(`
const socketReq = new Request("wss://host/socket");
new WebSocket(new Request("wss://host/direct"));
new WebSocket(socketReq);
navigator.sendBeacon(new Request("https://api.example.test/beacon"));
`),
).toEqual([
{
kind: "network",
value: 'new Request("wss://host/direct")',
mechanism: "call:WebSocket",
resolution: "partial",
},
{
kind: "network",
value: "socketReq",
mechanism: "call:WebSocket",
resolution: "partial",
},
{
kind: "network",
value: 'new Request("https://api.example.test/beacon")',
mechanism: "call:navigator.sendBeacon",
resolution: "partial",
},
]);
});
it("keeps Request objects partial when Request is locally shadowed", () => {
expect(
endpoints(`
function test(Request) {
fetch(new Request("/api/shadowed"));
}
`),
).toContainEqual({
kind: "network",
value: 'new Request("/api/shadowed")',
mechanism: "call:fetch",
resolution: "partial",
});
});
@@ -434,6 +434,26 @@ export const indexVitePreloadDependencyTable = (
return escapedOrMutated ? dependencies.map(() => undefined) : dependencies;
};
const isFetchCall = (
node: t.Node,
bindings: JavaScriptSemanticAnalysisState,
): boolean =>
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
isUnmodifiedGlobalReference(node.callee, bindings, "fetch");
const requestConstructorUrl = (
node: t.Node,
bindings: JavaScriptSemanticAnalysisState,
): string | undefined => {
const target = unwrapJavaScriptExpression(node).node;
if (
t.isNewExpression(target) &&
isUnmodifiedGlobalReference(target.callee, bindings, "Request")
)
return stringValue(target.arguments[0]);
return undefined;
};
const inspectEndpointCall = (
node: JavaScriptCallLike,
name: string,
@@ -444,7 +464,11 @@ const inspectEndpointCall = (
if (isKnownNetworkRequest(node, bindings)) {
const target = argumentNode(node.arguments[0]);
if (target === undefined) return;
const literal = stringValue(target);
const literal =
stringValue(target) ??
(isFetchCall(node, bindings)
? requestConstructorUrl(target, bindings)
: undefined);
if (literal !== undefined) {
addEndpoint(context, {
node,
@@ -529,11 +553,7 @@ const isKnownNetworkRequest = (
node: JavaScriptCallLike,
bindings: JavaScriptSemanticAnalysisState,
): boolean => {
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
isUnmodifiedGlobalReference(node.callee, bindings, "fetch")
)
return true;
if (isFetchCall(node, bindings)) return true;
if (
t.isNewExpression(node) &&
(isUnmodifiedGlobalReference(node.callee, bindings, "WebSocket") ||
@@ -655,6 +675,7 @@ const urlObjectRequestTarget = (
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptBindingIndex,
seen = new Set<string>(),
isToString = false,
): string | undefined => {
let target = unwrapJavaScriptExpression(node).node;
if (
@@ -666,8 +687,10 @@ const urlObjectRequestTarget = (
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
semanticStaticPropertyName(callee.property, callee.computed) ===
"toString"
)
) {
target = unwrapJavaScriptExpression(callee.object).node;
isToString = true;
}
}
if (!t.isIdentifier(target) || index.referenceMayHaveChangedAt(site, target))
return undefined;
@@ -688,7 +711,18 @@ const urlObjectRequestTarget = (
if (initializer === undefined) return undefined;
const initialized = unwrapJavaScriptExpression(initializer).node;
if (t.isIdentifier(initialized))
return urlObjectRequestTarget(initialized, site, bindings, index, seen);
return urlObjectRequestTarget(
initialized,
site,
bindings,
index,
seen,
isToString,
);
if (!isToString && isFetchCall(site, bindings)) {
const requestUrl = requestConstructorUrl(initialized, bindings);
if (requestUrl !== undefined) return requestUrl;
}
if (!t.isNewExpression(initialized)) return undefined;
if (!isUnmodifiedGlobalReference(initialized.callee, bindings, "URL"))
return undefined;