How to Work with HTTP Responses in Puppeteer
Wait for Puppeteer responses, inspect status and headers, read JSON or binary bodies, distinguish HTTP errors from network failures, and mock responses.
Puppeteer represents a response received by a Page with HTTPResponse. Use page.waitForResponse() to synchronize with a particular response, then inspect its status, headers, URL, matching request, metadata, or body. A 404 or 503 is still an HTTP response; it is different from a request that failed to load.
The examples below use the Puppeteer 25.x API shape documented for response waiting and inspection. Check the documentation for the exact Puppeteer version installed in your project: the indexed response, header, and content references span versions 25.9.0 through 25.12.0. See the HTTPResponse API, waitForResponse API, page events, and HTTPRequest.respond API.
Wait for a response triggered by an action
Start waiting before the action that triggers the request. If the response arrives quickly, this ordering ensures the waiter is already active. Match the request narrowly enough to avoid catching unrelated page traffic.
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/items') && response.status() === 200,
{ timeout: 10_000 }
);
await page.click('button.load-items');
const response = await responsePromise;
const payload = await response.json();
console.log(payload);
waitForResponse() accepts a URL or a predicate and resolves to the matching HTTPResponse. The documented default timeout is 30 seconds. You can supply a per-call timeout, configure a page default timeout, or pass an abort signal where supported by your installed version. A predicate can be asynchronous, but it should remain selective and resolve promptly.
If an action can fail before it triggers a request, handle that separately so a rejected click does not leave an unobserved waiter rejection. For a longer workflow, retain the response promise and make sure both the action and wait have explicit error handling.
Inspect status, URL, headers, and the matching request
const response = await page.waitForResponse(url => url.includes('/api/items'));
console.log('URL:', response.url());
console.log('Status:', response.status(), response.statusText());
console.log('Success:', response.ok());
console.log('Headers:', response.headers());
const request = response.request();
console.log('Method:', request.method());
console.log('Resource type:', request.resourceType());
console.log('Request URL:', request.url());
response.ok() is true for status codes from 200 through 299. The status code and status text are available even for HTTP error responses. The associated request exposes details such as its method, resource type, frame, and redirect chain.
Response headers are returned as an object with lowercase names. Duplicate header values are combined with commas, except Set-Cookie, whose values are separated by newlines. Do not assume every server sends every header your code might want to inspect.
Read a response body as JSON, text, or bytes
Choose the reader based on the actual payload and the next step in your code. Each body reader consumes response content; obtain the body once and reuse the parsed or buffered value rather than trying to read the same response through multiple methods.
| Method | Use it for | What can go wrong |
|---|---|---|
json() |
JSON API responses | Rejects if the body is not valid JSON. |
text() |
UTF-8 text such as HTML, XML, or plain text | Can fail if the content is not UTF-8. |
content() or buffer() |
Byte-oriented handling, such as saving or inspecting binary content | Browser re-encoding based on headers or heuristics can affect returned bytes. |
const response = await page.waitForResponse(r => r.url().includes('/api/items'));
if (!response.ok()) {
throw new Error(`API returned ${response.status()} ${response.statusText()}`);
}
const payload = await response.json();
console.log(payload.items);
For a text payload, use await response.text(). For bytes, use await response.content() or await response.buffer(), as available in your Puppeteer release. Treat the content type as a useful clue, not a guarantee that the body is valid JSON, UTF-8, or a particular binary format.
Tell HTTP errors apart from failed requests
A response with status 404 or 503 is a completed HTTP exchange. It is not ordinarily reported through the request-failure path. Check response.status() or response.ok() to handle HTTP-level errors. A request that fails while loading is reported through requestfailed. Redirects finish one request and start another to the redirected URL.
page.on('response', response => {
if (!response.ok()) {
console.warn('HTTP error response:', response.status(), response.url());
}
});
page.on('requestfailed', request => {
console.warn('Request failed to load:', request.url(), request.failure()?.errorText);
});
Use waitForResponse() when one operation must wait for a matching response. Use page-level response or requestfailed listeners when you need to observe ongoing page traffic. Remove listeners when the monitoring scope ends, especially in long-lived pages, so later activity does not trigger stale handlers.
Inspect response metadata and redirects
HTTPResponse also provides metadata methods for cache and service-worker status, timing, remote address, security details, and the associated frame. Availability and values can vary with the response and environment; handle nullable or unavailable results rather than assuming every field is populated.
const response = await page.waitForResponse(r => r.url().includes('/api/items'));
const request = response.request();
console.log('From cache:', response.fromCache());
console.log('From service worker:', response.fromServiceWorker());
console.log('Timing:', response.timing());
console.log('Remote address:', response.remoteAddress());
console.log('Security details:', response.securityDetails());
console.log('Frame:', response.frame());
console.log('Redirect chain:', request.redirectChain().map(item => item.url()));
Use these fields as diagnostic context, not as guaranteed measurements or proof of behavior across every browser and network setup. For redirects, inspect the request chain and final response URL to understand the route to the result.
Mock an HTTP response with request interception
Call page.setRequestInterception(true) before using request.respond(). With interception enabled, every intercepted request needs a resolution path. This example fulfills the matching API request and continues all others:
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/items')) {
void request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [] }),
}).catch(error => {
console.error('Could not fulfill request:', error);
});
} else {
void request.continue().catch(error => {
console.error('Could not continue request:', error);
});
}
});
Calling respond() without interception enabled throws; responding to a data URL request is a no-op. Keep handlers narrow, resolve each intercepted request, and account for async handler errors and interception coordination in the Puppeteer version you use. The snippet illustrates the API mechanism and is not a claim of having been run.
Or skip the browser setup
If your goal is to capture a page image or PDF rather than inspect an application response, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed; and its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options and setup. Sign up free for 1,000 screenshots a month, with no card required.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
waitForResponse() times out |
The action did not make the request, the URL or predicate is too strict, or the response took longer than the timeout. | Confirm the action and request URL, broaden only the necessary predicate, and set an appropriate timeout. Create the waiter before triggering the action. |
| The waiter catches the wrong response | Several requests share a URL fragment, or the predicate does not check enough properties. | Match a specific path and, where useful, method through response.request(), status, or another response property. |
A 404 does not fire requestfailed |
A 404 is an HTTP response and the request completed. | Listen for response or inspect the matching response’s status and ok(). |
json() rejects |
The response body is empty, malformed, or not JSON. | Inspect status and headers, then read with text() for diagnosis before parsing in a separate capture flow. |
text() fails or binary data looks wrong |
The body is not UTF-8 text, or browser decoding/re-encoding affected returned content. | Use a byte reader for binary-oriented work and verify the server’s content type and encoding. |
request.respond() throws |
Request interception was not enabled, or the request cannot be fulfilled in the current context. | Enable interception before handling requests; remember data URL responses are a no-op and resolve all intercepted requests. |
| Redirected URL differs from the URL requested | The browser followed a redirect, creating a new request. | Inspect the final response.url() and the request’s redirect chain. |
| Frame metadata is absent | Some responses, including navigation to error pages, can have no associated frame. | Handle a null frame and use response URL, status, and request details for diagnostics. |
Performance, reliability, and cost considerations
- Keep predicates selective. A precise URL and relevant response condition reduce accidental matches and make the intended synchronization clear.
- Bound waits. Choose a timeout suited to the workflow, and use cancellation when the operation is no longer needed. A timeout is a control on waiting, not proof that the server returned an error.
- Read only the needed body. Large bodies consume memory and can add parsing work. Avoid collecting response bodies for every request when only one endpoint matters.
- Separate HTTP outcomes from transport failures. Record status responses and request failures distinctly so retries and diagnosis reflect what happened.
- Clean up listeners and interception state. Long-running automation should avoid accumulating listeners and should ensure intercepted requests are resolved even when handlers encounter errors.
- Account for external costs. Puppeteer response inspection itself has no separate API fee specified by the cited API references. The browser, compute, proxy, and target services used by an automation system may have their own costs; estimate those from your deployment and providers rather than assuming a universal rate.
FAQ
How do I wait for an API response in Puppeteer?
Call page.waitForResponse() with a URL or predicate before triggering the UI action, then await the returned response promise.
How do I get a response body?
Use json() for JSON, text() for UTF-8 text, or content()/buffer() for bytes, and handle parsing or encoding errors.
How do I check a Puppeteer response status?
Call status() for the numeric status, statusText() for its text, and ok() for the 200–299 check.
Why does a 404 not trigger requestfailed?
Because the server returned an HTTP response and the request completed. Inspect the response event and status code instead.
Can I mock a response in Puppeteer?
Yes. Enable request interception, then call request.respond() for the request you want to fulfill and continue or otherwise resolve the others.


