Why does `page.waitForResponse()` timeout while `page.on('response')` finds the XHR response in Puppeteer?

Why does page.waitForResponse() timeout while page.on('response') finds the XHR response in Puppeteer?

I’m trying to capture a specific XHR response from a webpage using Puppeteer. Using

await page.waitForResponse(url);

or

await page.waitForResponse(res => res.url() === myUrl);

always times out. However, if I use

page.on('response', res => {
  if (res.url() === myUrl) {
    // handle response
  }
});

the response is correctly detected and handled.

I suspect that waitForResponse() isn’t picking up some XHR requests, possibly due to Puppeteer’s timing or the need for puppeteer-extra-plugin-stealth. How can I reliably wait for this response without missing it?

Having debugged this kind of Puppeteer timing issue quite a few times, what usually happens is that page.waitForResponse() simply isn’t ‘listening’ early enough. The request fires before the wait is attached, so Puppeteer never sees it.

One workaround I’ve used is to wrap page.on('response') inside a Promise basically recreating the wait logic manually:

const waitForMyResponse = new Promise(resolve => {
  page.on('response', res => {
    if (res.url() === myUrl && res.status() === 200) {
      resolve(res);
    }
  });
});

// Trigger action
await page.click('#trigger-button');

const response = await waitForMyResponse;
const data = await response.json();
console.log(data);

This approach catches the response reliably, especially when the XHR fires immediately after page load.

@tim-khorev’s spot on. I’ve also run into this during debugging sessions, and the main culprit is almost always registration order. page.waitForResponse() only works if it’s set up before the action triggers the request. Otherwise, Puppeteer has no chance of observing the XHRit’s already gone by the time the listener is attached.

Here’s the pattern I rely on:

const [response] = await Promise.all([
  page.waitForResponse(res => res.url() === myUrl && res.status() === 200),
  page.click('#trigger-button') // XHR starts here
]);

const data = await response.json();

Bundling them with Promise.all() ensures the listener is active before the click happens, so it never misses the request.

Both of you make great points. I’ll add one more thing I’ve learned from working with APIs that love to append timestamps, tokens, or random query params. Even when timing is perfect, page.waitForResponse() may still timeout simply because the URL you’ve hard-matched doesn’t match exactly what the browser sends.

In those cases, using a predicate with partial matching is much more reliable:

await page.waitForResponse(res => {
  return res.url().includes('api/data') && res.status() === 200;
});

This way, dynamic query strings or hashes won’t break your wait logic, and waitForResponse() resolves exactly when the right XHR arrives