| 194 | // of the `act` scope. It will be removed before `act` exits. |
| 195 | let onDidIssueFirstRequest: (() => void) | null = null |
| 196 | const routeHandler = async (route: Playwright.Route) => { |
| 197 | const request = route.request() |
| 198 | |
| 199 | const pendingRequests = batch.pendingRequests |
| 200 | const pendingRequestChecks = batch.pendingRequestChecks |
| 201 | |
| 202 | // Because determining whether we need to intercept the request is an |
| 203 | // async operation, we collect these promises so we can await them at the |
| 204 | // end of the `act` scope to see whether any additional requests |
| 205 | // were initiated. |
| 206 | // NOTE: The default check doesn't actually need to be async, but since |
| 207 | // this logic is subtle, to preserve the ability to add an async |
| 208 | // check later, I'm treating it as if it could possibly be async. |
| 209 | const checkIfRouterRequest = (async () => { |
| 210 | const headers = request.headers() |
| 211 | |
| 212 | // The default check includes navigations, prefetches, and actions. |
| 213 | const isRouterRequest = |
| 214 | headers['rsc'] !== undefined || // Matches navigations and prefetches |
| 215 | headers['next-action'] !== undefined // Matches Server Actions |
| 216 | |
| 217 | if (isRouterRequest) { |
| 218 | // This request was initiated by the Next.js Router. Intercept it and |
| 219 | // add it to the current batch. |
| 220 | pendingRequests.add({ |
| 221 | url: request.url(), |
| 222 | route, |
| 223 | // `act` controls the timing of when responses reach the client, |
| 224 | // but it should not affect the timing of when requests reach the |
| 225 | // server; we pass the request to the server the immediately. |
| 226 | result: (async () => { |
| 227 | let originalResponse: Playwright.APIResponse |
| 228 | try { |
| 229 | originalResponse = await page.request.fetch(request, { |
| 230 | maxRedirects: 0, |
| 231 | }) |
| 232 | } catch (fetchError) { |
| 233 | error.message = |
| 234 | fetchError instanceof Error |
| 235 | ? fetchError.message |
| 236 | : String(fetchError) |
| 237 | throw error |
| 238 | } |
| 239 | |
| 240 | // WORKAROUND: |
| 241 | // intercepting responses with 'Transfer-Encoding: chunked' (used for streaming) |
| 242 | // seems to be problematic sometimes, making the browser error with `net::ERR_INCOMPLETE_CHUNKED_ENCODING`. |
| 243 | // In particular, this seems to happen when blocking a streaming navigation response. (but not always) |
| 244 | // Playwright buffers the whole body anyway, so we can remove the header to sidestep this. |
| 245 | const headers = originalResponse.headers() |
| 246 | delete headers['transfer-encoding'] |
| 247 | |
| 248 | return { |
| 249 | text: await originalResponse.text(), |
| 250 | body: await originalResponse.body(), |
| 251 | headers, |
| 252 | status: originalResponse.status(), |
| 253 | } |