mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
fix: let resourceTypes match CORS preflights and prefetches (#3217)
Fixes #2954. `list_network_requests` accepts `resourceTypes: ["preflight"]` and `["prefetch"]`, but neither ever matched anything. I checked with a CDP session: Chrome itself sends `Network.requestWillBeSent` with `type: "Other"` for the CORS preflight `OPTIONS` and for a `<link rel=prefetch>` request, so Puppeteer's `resourceType()` returns `other` for both. This adds `getFilterableResourceType()` (`src/utils/resourceType.ts`), which the filter now uses. It only looks past `other`: - `OPTIONS` with an `Access-Control-Request-Method` header (the headers that make a request a CORS preflight) → `preflight` - a `Sec-Purpose: prefetch…` header, which Chrome sends on speculative prefetches → `prefetch` - anything else keeps Chrome's type, so `["other"]` still matches e.g. `favicon.ico`, and a plain `OPTIONS` without the CORS header stays `other`. Tests: - `McpResponse` (real page): a page prefetches `/data?prefetch` and sends a cross-origin `PUT` with a custom header (`127.0.0.1` → `localhost`). `["preflight"]` returns only the `OPTIONS`, `["prefetch"]` only the prefetch, and `["fetch"]` only the `PUT`. This fails on `main` and passes with the change. - `tests/utils/resourceType.test.ts`: unit cases for both rules and the cases that must stay `other`. - `npm run build`, `eslint src tests` and `prettier --check .` are clean. The `McpResponse` suite passes. _AI-assisted: I used an AI assistant to help write this change. I reviewed the code and tests and ran them locally._
This commit is contained in:
1 parent
b8b1f13f15
commit
e342c6e21b
4 files changed
+136
-2
No files matched your search
+2
-2
@@ -73,6 +73,7 @@ import type {CD4ACommentThread, PaginationOptions} from './types.js';
|
||||
import type {WithSymbolId} from './utils/id.js';
|
||||
import {stableIdSymbol} from './utils/id.js';
|
||||
import {paginate} from './utils/pagination.js';
|
||||
import {getFilterableResourceType} from './utils/resourceType.js';
|
||||
import type {WaitForEventsResult} from './utils/WaitForHelper.js';
|
||||
|
||||
const {formatBytesToKb} = DevTools.I18n.ByteUtilities;
|
||||
@@ -726,8 +727,7 @@ export class McpResponse implements Response {
|
||||
this.#networkRequestsOptions.resourceTypes,
|
||||
);
|
||||
requests = requests.filter(request => {
|
||||
const type = request.resourceType();
|
||||
return normalizedTypes.has(type);
|
||||
return normalizedTypes.has(getFilterableResourceType(request));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {HTTPRequest, ResourceType} from '../third_party/index.js';
|
||||
|
||||
/**
|
||||
* The resource type a request is filtered by. Chrome reports CORS preflights
|
||||
* and `<link rel=prefetch>` requests as `Other`, so `resourceTypes: ['preflight']`
|
||||
* and `['prefetch']` could never match. Those two are recognised from the
|
||||
* headers Chrome sends with them; every other type is Chrome's own.
|
||||
*/
|
||||
export function getFilterableResourceType(request: HTTPRequest): ResourceType {
|
||||
const type = request.resourceType();
|
||||
if (type !== 'other') {
|
||||
return type;
|
||||
}
|
||||
const headers = request.headers();
|
||||
if (
|
||||
request.method() === 'OPTIONS' &&
|
||||
headers['access-control-request-method'] !== undefined
|
||||
) {
|
||||
return 'preflight';
|
||||
}
|
||||
if (/^prefetch\b/i.test(headers['sec-purpose'] ?? '')) {
|
||||
return 'prefetch';
|
||||
}
|
||||
return type;
|
||||
}
|
||||
@@ -697,6 +697,58 @@ describe('McpResponse', () => {
|
||||
});
|
||||
|
||||
describe('McpResponse network request filtering', () => {
|
||||
const server = serverHooks();
|
||||
|
||||
it('filters CORS preflights and prefetches by their own types', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
server.addRoute('/data?prefetch', (_req, res) => {
|
||||
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||
res.end('prefetched');
|
||||
});
|
||||
server.addRoute('/data?cors', (_req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'PUT',
|
||||
'Access-Control-Allow-Headers': 'x-custom',
|
||||
'Content-Type': 'text/plain',
|
||||
});
|
||||
res.end('cors');
|
||||
});
|
||||
// localhost vs 127.0.0.1 makes the PUT cross-origin, so it is preflighted.
|
||||
const crossOrigin = server
|
||||
.getRoute('/data?cors')
|
||||
.replace('127.0.0.1', 'localhost');
|
||||
server.addHtmlRoute(
|
||||
'/types.html',
|
||||
`<!doctype html><link rel="prefetch" href="/data?prefetch"><script>fetch(${JSON.stringify(crossOrigin)}, {method: 'PUT', headers: {'x-custom': '1'}});</script>`,
|
||||
);
|
||||
const page = context.getSelectedMcpPage().pptrPage;
|
||||
const done = Promise.all([
|
||||
page.waitForResponse(r => r.url().endsWith('/data?prefetch')),
|
||||
page.waitForResponse(
|
||||
r => r.url() === crossOrigin && r.request().method() === 'PUT',
|
||||
),
|
||||
]);
|
||||
await page.goto(server.getRoute('/types.html'));
|
||||
await done;
|
||||
|
||||
response.setIncludeNetworkRequests(true, {resourceTypes: ['preflight']});
|
||||
let text = getTextContent((await response.handle(context)).content[0]);
|
||||
assert.match(text, /OPTIONS \S+\/data\?cors/, text);
|
||||
assert.doesNotMatch(text, /PUT|prefetch/, text);
|
||||
|
||||
response.setIncludeNetworkRequests(true, {resourceTypes: ['prefetch']});
|
||||
text = getTextContent((await response.handle(context)).content[0]);
|
||||
assert.match(text, /GET \S+\/data\?prefetch/, text);
|
||||
assert.doesNotMatch(text, /OPTIONS|PUT/, text);
|
||||
|
||||
response.setIncludeNetworkRequests(true, {resourceTypes: ['fetch']});
|
||||
text = getTextContent((await response.handle(context)).content[0]);
|
||||
assert.match(text, /PUT \S+\/data\?cors/, text);
|
||||
assert.doesNotMatch(text, /OPTIONS|prefetch/, text);
|
||||
});
|
||||
});
|
||||
|
||||
it('filters network requests by resource type', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
response.setIncludeNetworkRequests(true, {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import {getFilterableResourceType} from '../../src/utils/resourceType.js';
|
||||
import {getMockRequest} from '../utils.js';
|
||||
|
||||
describe('getFilterableResourceType', () => {
|
||||
it('keeps the type Chrome reported', () => {
|
||||
assert.strictEqual(
|
||||
getFilterableResourceType(getMockRequest({resourceType: 'fetch'})),
|
||||
'fetch',
|
||||
);
|
||||
});
|
||||
|
||||
it('recognises a CORS preflight reported as other', () => {
|
||||
const request = getMockRequest({
|
||||
method: 'OPTIONS',
|
||||
resourceType: 'other',
|
||||
headers: {'access-control-request-method': 'PUT'},
|
||||
});
|
||||
assert.strictEqual(getFilterableResourceType(request), 'preflight');
|
||||
});
|
||||
|
||||
it('keeps a plain OPTIONS request as other', () => {
|
||||
const request = getMockRequest({
|
||||
method: 'OPTIONS',
|
||||
resourceType: 'other',
|
||||
headers: {},
|
||||
});
|
||||
assert.strictEqual(getFilterableResourceType(request), 'other');
|
||||
});
|
||||
|
||||
it('recognises a prefetch reported as other', () => {
|
||||
const request = getMockRequest({
|
||||
resourceType: 'other',
|
||||
headers: {'sec-purpose': 'prefetch'},
|
||||
});
|
||||
assert.strictEqual(getFilterableResourceType(request), 'prefetch');
|
||||
});
|
||||
|
||||
it('keeps an unrelated other request as other', () => {
|
||||
const request = getMockRequest({resourceType: 'other', headers: {}});
|
||||
assert.strictEqual(getFilterableResourceType(request), 'other');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user