88 * The color mode is determined by the LIGHTHOUSE_COLOR_MODE environment variable.
99 * If not set, defaults to 'dark'.
1010 *
11- * Request interception uses CDP (Chrome DevTools Protocol) at the browser level
12- * so it applies to all pages Lighthouse opens, not just the setup page.
11+ * Request interception uses CDP (Chrome DevTools Protocol) Fetch domain
12+ * at the browser level, which avoids conflicts with Lighthouse's own
13+ * Puppeteer-level request interception.
1314 */
1415
1516const mockRoutes = require ( './test/fixtures/mock-routes.cjs' )
1617
1718module . exports = async function setup ( browser , { url } ) {
1819 const colorMode = process . env . LIGHTHOUSE_COLOR_MODE || 'dark'
1920
20- // Set up browser-level request interception via CDP.
21- // This ensures mocking applies to pages Lighthouse creates after setup.
22- setupBrowserRequestInterception ( browser )
21+ // Set up browser-level request interception via CDP Fetch domain.
22+ // This operates below Puppeteer's request interception layer so it
23+ // doesn't conflict with Lighthouse's own setRequestInterception usage.
24+ await setupCdpRequestInterception ( browser )
2325
2426 const page = await browser . newPage ( )
2527
@@ -36,37 +38,52 @@ module.exports = async function setup(browser, { url }) {
3638}
3739
3840/**
39- * Set up request interception on every new page target the browser creates.
40- * Uses Puppeteer's page-level request interception, applied automatically
41- * to each new page via the 'targetcreated' event.
41+ * Set up request interception using CDP's Fetch domain on the browser's
42+ * default context. This intercepts requests at a lower level than Puppeteer's
43+ * page.setRequestInterception(), avoiding "Request is already handled!" errors
44+ * when Lighthouse sets up its own interception.
4245 *
4346 * @param {import('puppeteer').Browser } browser
4447 */
45- function setupBrowserRequestInterception ( browser ) {
48+ async function setupCdpRequestInterception ( browser ) {
49+ // Build URL pattern list for CDP Fetch.enable from our route definitions
50+ const cdpPatterns = mockRoutes . routes . map ( route => ( {
51+ urlPattern : route . pattern . replace ( '/**' , '/*' ) ,
52+ requestStage : 'Request' ,
53+ } ) )
54+
55+ // Listen for new targets so we can attach CDP interception to each page
4656 browser . on ( 'targetcreated' , async target => {
4757 if ( target . type ( ) !== 'page' ) return
4858
4959 try {
50- const page = await target . page ( )
51- if ( ! page ) return
60+ const cdp = await target . createCDPSession ( )
5261
53- await page . setRequestInterception ( true )
54- page . on ( 'request' , request => {
55- const requestUrl = request . url ( )
62+ cdp . on ( 'Fetch.requestPaused' , async event => {
63+ const requestUrl = event . request . url
5664 const result = mockRoutes . matchRoute ( requestUrl )
5765
5866 if ( result ) {
59- request . respond ( {
60- status : result . response . status ,
61- contentType : result . response . contentType ,
62- body : result . response . body ,
67+ const body = Buffer . from ( result . response . body ) . toString ( 'base64' )
68+ await cdp . send ( 'Fetch.fulfillRequest' , {
69+ requestId : event . requestId ,
70+ responseCode : result . response . status ,
71+ responseHeaders : [
72+ { name : 'Content-Type' , value : result . response . contentType } ,
73+ { name : 'Access-Control-Allow-Origin' , value : '*' } ,
74+ ] ,
75+ body,
6376 } )
6477 } else {
65- request . continue ( )
78+ await cdp . send ( 'Fetch.continueRequest' , {
79+ requestId : event . requestId ,
80+ } )
6681 }
6782 } )
83+
84+ await cdp . send ( 'Fetch.enable' , { patterns : cdpPatterns } )
6885 } catch {
69- // Target may have been closed before we could set up interception .
86+ // Target may have been closed before we could attach .
7087 // This is expected for transient targets like service workers.
7188 }
7289 } )
0 commit comments