Skip to content

Commit 2d7cd7e

Browse files
vzaidmanfacebook-github-bot
authored andcommitted
Forward getScriptSource with no fetchable URL (#58625)
Summary: The proxy intercepted every `Debugger.getScriptSource` request and tried to fetch the URL it had recorded for the script, failing closed on an empty or non-HTTP one. Scripts compiled from debug client expressions (e.g. code typed into the DevTools console) report an empty `url`, so requesting their source failed even once the target can serve it. The proxy now forwards the request to the target when it has no HTTP(S) URL of its own to fetch, and relays the target's response back. Proxy-side serving is unchanged: with a fetchable URL the proxy still answers itself, and it still never reads local files. Changelog: [GENERAL] [FIXED] - Fix "Unable to fetch script source" when debugging code evaluated in the DevTools console Differential Revision: D121001393
1 parent f03f6c2 commit 2d7cd7e

2 files changed

Lines changed: 85 additions & 26 deletions

File tree

packages/dev-middleware/src/__tests__/InspectorProxyCdpRewritingHacks-test.js

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
withServerForEachTest,
2222
} from './ServerUtils';
2323
import {createHash} from 'node:crypto';
24+
import until from 'wait-for-expect';
2425

2526
// WebSocket is unreliable when using fake timers.
2627
jest.useRealTimers();
@@ -470,8 +471,8 @@ describe.each(['HTTP', 'HTTPS'])(
470471
}
471472
});
472473

473-
test('throws when attempting to pass a filesystem url', async () => {
474-
const {device, debugger_} = await createAndConnectTarget(
474+
test('forwards to the target for a url the proxy cannot fetch', async () => {
475+
const {device, debugger_, sessionId} = await createAndConnectTarget(
475476
serverRef,
476477
autoCleanup.signal,
477478
{
@@ -495,36 +496,81 @@ describe.each(['HTTP', 'HTTPS'])(
495496
hash: createHash('sha256').update('').digest('hex'),
496497
},
497498
});
498-
const response = await debugger_.sendAndGetResponse({
499+
const message = {
499500
id: 1,
500501
method: 'Debugger.getScriptSource',
501502
params: {
502503
scriptId: 'script1',
503504
},
505+
};
506+
await sendFromDebuggerToTarget(debugger_, device, 'page1', message, {
507+
sessionId,
504508
});
505-
expect(response.result).toEqual(
506-
expect.objectContaining({
507-
error: {
508-
message: expect.stringContaining(
509-
'Can\'t parse requested URL "__fixtures__/mock-source-file.txt"',
510-
),
511-
},
512-
}),
513-
);
514509

515-
// The device does not receive the getScriptSource request, since it
516-
// is handled by the proxy.
517-
expect(device.wrappedEventParsed).not.toBeCalledWith({
510+
// The proxy only fetches HTTP(S) urls itself, so rather than failing
511+
// it hands the request to the target.
512+
expect(device.wrappedEventParsed).toBeCalledWith({
518513
pageId: 'page1',
519-
wrappedEvent: expect.objectContaining({
520-
method: 'Debugger.getScriptSource',
521-
}),
514+
sessionId,
515+
wrappedEvent: message,
522516
});
523517
} finally {
524518
device.close();
525519
debugger_.close();
526520
}
527521
});
522+
523+
test('forwards to the target for a script with no url', async () => {
524+
const {device, debugger_, sessionId} = await createAndConnectTarget(
525+
serverRef,
526+
autoCleanup.signal,
527+
{
528+
app: 'bar-app',
529+
id: 'page1',
530+
title: 'bar-title',
531+
vm: 'bar-vm',
532+
},
533+
);
534+
535+
try {
536+
// Targets report an empty url for code they compiled from a debugger
537+
// expression, such as code typed into the DevTools console.
538+
await sendFromTargetToDebugger(device, debugger_, 'page1', {
539+
method: 'Debugger.scriptParsed',
540+
params: {
541+
scriptId: 'script1',
542+
url: '',
543+
},
544+
});
545+
const message = {
546+
id: 1,
547+
method: 'Debugger.getScriptSource',
548+
params: {
549+
scriptId: 'script1',
550+
},
551+
};
552+
await sendFromDebuggerToTarget(debugger_, device, 'page1', message, {
553+
sessionId,
554+
});
555+
expect(device.wrappedEventParsed).toBeCalledWith({
556+
pageId: 'page1',
557+
sessionId,
558+
wrappedEvent: message,
559+
});
560+
561+
// The target answers, and the proxy relays that back to the debugger.
562+
const response = {id: 1, result: {scriptSource: 'debugger;'}};
563+
device.sendWrappedEvent('page1', response);
564+
await until(() =>
565+
expect(debugger_.handle).toBeCalledWith(
566+
expect.objectContaining(response),
567+
),
568+
);
569+
} finally {
570+
device.close();
571+
debugger_.close();
572+
}
573+
});
528574
});
529575

530576
describe("disabled when target has 'nativeSourceCodeFetching' capability flag", () => {

packages/dev-middleware/src/inspector-proxy/Device.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,12 @@ export default class Device {
953953
case 'Debugger.setBreakpointByUrl':
954954
return this.#processDebuggerSetBreakpointByUrl(req, debuggerInfo);
955955
case 'Debugger.getScriptSource':
956+
if (!this.#hasFetchableScriptSource(req.params.scriptId)) {
957+
// Forward to the target, which is the only one that can still have
958+
// the source - for instance for code the user typed into the
959+
// DevTools console, which the target compiled without a URL.
960+
return req;
961+
}
956962
// Sends response to debugger via side-effect
957963
void this.#processDebuggerGetScriptSource(req, socket, debuggerInfo);
958964
return null;
@@ -1038,6 +1044,15 @@ export default class Device {
10381044
return processedReq;
10391045
}
10401046

1047+
/**
1048+
* Whether the proxy recorded an HTTP(S) source URL for a script, and can
1049+
* therefore serve its source itself by fetching that URL.
1050+
*/
1051+
#hasFetchableScriptSource(scriptId: string): boolean {
1052+
const pathToSource = this.#scriptIdToSourcePathMapping.get(scriptId);
1053+
return pathToSource != null && this.#tryParseHTTPURL(pathToSource) != null;
1054+
}
1055+
10411056
async #processDebuggerGetScriptSource(
10421057
req: CDPRequest<'Debugger.getScriptSource'>,
10431058
socket: WS,
@@ -1077,16 +1092,14 @@ export default class Device {
10771092
const pathToSource = this.#scriptIdToSourcePathMapping.get(
10781093
req.params.scriptId,
10791094
);
1095+
const httpURL =
1096+
pathToSource != null ? this.#tryParseHTTPURL(pathToSource) : null;
1097+
invariant(
1098+
httpURL != null,
1099+
'processDebuggerGetScriptSource called for non-fetchable script',
1100+
);
10801101

10811102
try {
1082-
const httpURL =
1083-
pathToSource == null ? null : this.#tryParseHTTPURL(pathToSource);
1084-
if (!httpURL) {
1085-
throw new Error(
1086-
`Can't parse requested URL ${pathToSource === undefined ? 'undefined' : JSON.stringify(pathToSource)}`,
1087-
);
1088-
}
1089-
10901103
const text = await this.#fetchText(httpURL);
10911104

10921105
sendSuccessResponse(text);

0 commit comments

Comments
 (0)