Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4c94081
chore: upgrade Chromium from v126 to v143 for asset discovery
Manoj-Katta Apr 17, 2026
c3b1f16
test(core): ignore favicon.ico in test server request log
Manoj-Katta Apr 19, 2026
319744f
fix(core): disable site isolation for Chrome 143 new headless
Manoj-Katta Apr 19, 2026
1b7f936
chore(ci): bump cache-key for Chromium v143 upgrade
Manoj-Katta Apr 20, 2026
f64f1f8
test(core): align test helpers with Chrome new-headless behavior (PPL…
Manoj-Katta Apr 27, 2026
5ba42dc
fix(core): disable Local Network Access checks for Chrome 143 (PPLT-5…
Manoj-Katta Apr 27, 2026
4f02ad6
chore(core): document Chrome 143 upgrade decisions per PR review
Manoj-Katta Apr 27, 2026
56306df
fix(core): handle Chrome 143 split request lifecycle and malformed Co…
Manoj-Katta Apr 27, 2026
6a5af29
fix(core): preserve original errorText for response-stage Fetch error…
Manoj-Katta Apr 28, 2026
890883a
test(core): cover favicon capture path explicitly
Manoj-Katta Apr 28, 2026
bd44f96
refactor(core): capture body at Fetch response stage instead of using…
Manoj-Katta Apr 28, 2026
98f5a4e
test(core): cover saveResponseResource catch and auto-fetched favicon…
Manoj-Katta Apr 29, 2026
96a014b
fix(core): guard Network constructor against null session.browser (PP…
Manoj-Katta Apr 29, 2026
0cad47f
test(core): cover untracked-request branch and simplify error message…
Manoj-Katta Apr 30, 2026
a0638fe
test(core): stabilize two timing-fragile tests under suite load (PPLT…
Manoj-Katta May 3, 2026
46025d7
test(core): attach catch handler synchronously on sync-snapshot promi…
Manoj-Katta May 3, 2026
563da4c
test(core): bump waitForTimeout to give failing upload reliable headr…
Manoj-Katta May 3, 2026
e09ee41
test(core): cover oversized/malformed branch for untracked requests (…
Manoj-Katta May 4, 2026
9ec37ac
fix(core): timeout responseReceived wait so worker scripts don't leak…
Manoj-Katta May 4, 2026
4a4252a
test(core): drop trailing blank line that broke yarn lint (PPLT-4214)
Manoj-Katta May 4, 2026
2bb505d
fix(core): make browser-launch failure path testable across Chrome ve…
Manoj-Katta May 7, 2026
b2fd751
fix(core): make worker-script responseReceived timeout observable (PP…
Manoj-Katta May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/.cache-key
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
; ;
/ \
_____________/_ __ \_____________
Times we have broken CI: 3
Times we have broken CI: 4
Times Windows has broken CI: 99+
13 changes: 9 additions & 4 deletions packages/core/src/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ export class Browser extends EventEmitter {
#lastid = 0;

args = [
// disable the translate popup and optimization downloads
'--disable-features=Translate,OptimizationGuideModelDownloading',
// Disable Chrome features that break asset discovery in v143 new-headless:
// site/origin isolation (so cross-origin events stay on the page session),
// HTTPS-first auto-upgrade (would block HTTP discovery), and Local Network
// Access permission checks (would block sub-resources to localhost/RFC1918).
'--disable-features=Translate,OptimizationGuideModelDownloading,IsolateOrigins,site-per-process,HttpsFirstBalancedModeAutoEnable,LocalNetworkAccessChecks',
// disable several subsystems which run network requests in the background
'--disable-background-networking',
// disable task throttling of timer tasks from background pages
Expand Down Expand Up @@ -276,9 +279,11 @@ export class Browser extends EventEmitter {
if (match) cleanup(() => resolve(match[1]));
};

let handleExitClose = () => handleError();
let handleExitClose = () => handleError(
new Error('Browser exited before devtools address')
);
let handleError = error => cleanup(() => reject(new Error(
`Failed to launch browser. ${error?.message ?? ''}\n${stderr}'\n\n`
`Failed to launch browser. ${error.message}\n${stderr}'\n\n`
)));

let cleanup = callback => {
Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,14 @@ export function chromium({
});
}

// default chromium revisions corresponds to v126.0.6478.184
// Chrome 143.0.7499.169 (base position 1536371) — closest per-platform
// revision from https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html
chromium.revisions = {
linux: '1300309',
win64: '1300297',
win32: '1300295',
darwin: '1300293',
darwinArm: '1300314'
linux: '1536366',
win64: '1536376',
win32: '1536377',
darwin: '1536380',
darwinArm: '1536376'
Comment on lines +181 to +185
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you got it this, can you attach a reference here ?

};

// export the namespace by default
Expand Down
114 changes: 96 additions & 18 deletions packages/core/src/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const MAX_RESOURCE_SIZE = 25 * (1024 ** 2) * 0.63; // 25MB, 0.63 factor for acco
const ALLOWED_STATUSES = [200, 201, 301, 302, 304, 307, 308];
const ALLOWED_RESOURCES = ['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Other'];
const ABORTED_MESSAGE = 'Request was aborted by browser';
// Chrome 143 omits Network.responseReceived for worker scripts; cap the wait so loadingFinished can clean up.
const RESPONSE_RECEIVED_TIMEOUT = 2000;

// RequestLifeCycleHandler handles life cycle of a requestId
// Ideal flow: requestWillBeSent -> requestPaused -> responseReceived -> loadingFinished / loadingFailed
Expand Down Expand Up @@ -41,7 +43,7 @@ export class Network {
this.captureMockedServiceWorker = options.captureMockedServiceWorker ?? false;
this.userAgent = options.userAgent ??
// by default, emulate a non-headless browser
page.session.browser.version.userAgent.replace('Headless', '');
page.session.browser?.version?.userAgent?.replace('Headless', '');
this.fontDomains = options.fontDomains || [];
this.intercept = options.intercept;
this.meta = options.meta;
Expand Down Expand Up @@ -205,6 +207,13 @@ export class Network {
_handleRequestPaused = async (session, event) => {
let { networkId: requestId, requestId: interceptId, resourceType } = event;

// Response-stage events arrive here when Fetch.continueRequest was called
// with interceptResponse:true (see sendResponseResource).
if (event.responseStatusCode != null || event.responseErrorReason != null) {
await this._handleResponsePaused(session, event);
return;
}

// wait for request to be sent
await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
let pending = this.#pending.get(requestId);
Expand All @@ -217,6 +226,53 @@ export class Network {
await this._handleRequest(session, { ...pending, resourceType, interceptId });
}

// Response-stage interception is kept ONLY to detect oversized/malformed
// Content-Length and abort the request before Chrome streams a body it
// would never terminate (Chrome 143 quirk). For everything else we just
// continue — body capture happens later via Network.loadingFinished →
// Network.getResponseBody (the v126 path). Reading the body at this stage
// hangs worker-initiated fetches, so we don't.
_handleResponsePaused = async (session, event) => {
let { networkId: requestId, requestId: interceptId, responseHeaders, responseStatusCode } = event;
let request = this.#requests.get(requestId);
let url = request ? originURL(request) : event.request?.url;
let headersObj = headersArrayToObject(responseHeaders);
let { tooLarge, malformed, rawValue } = inspectContentLength(headersObj);

if (tooLarge || malformed) {
let meta = { ...this.meta, url, responseStatus: responseStatusCode };
logAssetInstrumentation(this.log, 'asset_not_uploaded', 'resource_too_large', {
url, size: rawValue, snapshot: meta.snapshot
});
this.log.debug('- Skipping resource larger than 25MB', meta);
if (request) {
this._forgetRequest(request);
this.#requestsLifeCycleHandler.get(requestId).resolveResponseReceived();
}
try {
await this.send(session, 'Fetch.failRequest', { requestId: interceptId, errorReason: 'Aborted' });
} catch (error) {
/* istanbul ignore next: race with abort/close */
this.log.debug(`Failed to abort oversized response for ${url}: ${error.message}`);
}
return;
}

return this._continueResponse(session, interceptId, url);
}

// Tell the browser to continue the paused response, swallowing expected
// races (request already aborted, interception ID no longer valid).
_continueResponse = async (session, interceptId, url) => {
try {
await this.send(session, 'Fetch.continueResponse', { requestId: interceptId });
} catch (error) {
/* istanbul ignore next: race with abort/close */
if (error.message === ABORTED_MESSAGE || error.message.includes('Invalid InterceptionId')) return;
this.log.debug(`Failed to continue response for ${url}: ${error.message}`);
}
}

// Called when a request will be sent. If the request has already been intercepted, handle it;
// otherwise set it to be pending until it is paused.
_handleRequestWillBeSent = async event => {
Expand Down Expand Up @@ -306,12 +362,25 @@ export class Network {
// callback. The request should have an associated response and be finished with any redirects.
_handleLoadingFinished = async (session, event) => {
let { requestId } = event;
// wait for upto 2 seconds or check if response has been sent
await this.#requestsLifeCycleHandler.get(requestId).responseReceived;
let request = this.#requests.get(requestId);
/* istanbul ignore if: race condition paranoia */
if (!request) return;

if (!request.response) {
let timerId;
await Promise.race([
this.#requestsLifeCycleHandler.get(requestId).responseReceived,
new Promise(resolve => { timerId = setTimeout(resolve, RESPONSE_RECEIVED_TIMEOUT); })
]);
clearTimeout(timerId);
}

if (!request.response) {
this.log.debug(`Skipping resource: responseReceived not received within ${RESPONSE_RECEIVED_TIMEOUT}ms - ${request.url}`);
this._forgetRequest(request);
return;
}

await saveResponseResource(this, request, session);
this._forgetRequest(request);
}
Expand Down Expand Up @@ -434,6 +503,24 @@ function originURL(request) {
return normalizeURL((request.redirectChain[0] || request).url);
}

// Convert Fetch event responseHeaders ([{name, value}, …]) to a header object.
function headersArrayToObject(arr) {
let out = {};
if (!Array.isArray(arr)) return out;
for (let { name, value } of arr) out[name] = value;
return out;
}

// Returns { tooLarge, malformed, rawValue } for Content-Length classification.
function inspectContentLength(headers) {
let key = headers && Object.keys(headers).find(k => k.toLowerCase() === 'content-length');
let rawValue = key ? headers[key] : undefined;
let parsed = parseInt(rawValue);
let tooLarge = Number.isFinite(parsed) && parsed > MAX_RESOURCE_SIZE;
let malformed = rawValue !== undefined && rawValue !== null && String(rawValue).length > 0 && !Number.isFinite(parsed);
return { tooLarge, malformed, rawValue };
}

// Validate domain for auto-allowlisting feature
// Only validates domains that returned 200 status
async function validateDomainForAllowlist(network, hostname, url, statusCode) {
Expand Down Expand Up @@ -512,8 +599,10 @@ async function sendResponseResource(network, request, session) {
.map(([k, v]) => ({ name: k.toLowerCase(), value: String(v) }))
});
} else {
// interceptResponse:true triggers a second pause at the response stage. See _handleResponsePaused.
await send('Fetch.continueRequest', {
requestId: request.interceptId
requestId: request.interceptId,
interceptResponse: true
});
}
} catch (error) {
Expand Down Expand Up @@ -558,7 +647,7 @@ async function makeDirectRequest(network, request, session) {
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'font',
'sec-ch-ua': '"Chromium";v="123", "Google Chrome";v="123", "Not?A_Brand";v="99"',
'sec-ch-ua': '"Chromium";v="143", "Google Chrome";v="143", "Not?A_Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-user': '?1',
Expand Down Expand Up @@ -590,19 +679,8 @@ async function saveResponseResource(network, request, session) {
url,
responseStatus: response?.status
};
// Checking for content length more than 100MB, to prevent websocket error which is governed by
// maxPayload option of websocket defaulted to 100MB.
// If content-length is more than our allowed 25MB, no need to process that resouce we can return log.
let contentLength = response.headers?.[Object.keys(response.headers).find(key => key.toLowerCase() === 'content-length')];
contentLength = parseInt(contentLength);
if (contentLength > MAX_RESOURCE_SIZE) {
logAssetInstrumentation(log, 'asset_not_uploaded', 'resource_too_large', {
url,
size: contentLength,
snapshot: meta.snapshot
});
return log.debug('- Skipping resource larger than 25MB', meta);
}
// Oversized/malformed Content-Length is rejected earlier in _handleResponsePaused;
// the body.length check below still guards cached responses where headers may lie.
let resource = network.intercept.getResource(url);

if (!resource || (!resource.root && !resource.provided && disableCache)) {
Expand Down
Loading
Loading