How to Trace Redirects in Puppeteer
Trace navigation redirects in Puppeteer with redirectChain(), log request events, and distinguish HTTP errors from failed requests.
To trace a navigation redirect in Puppeteer, navigate with page.goto(), then read the final response’s request chain with response.request().redirectChain(). The array contains the preceding requests in that resource chain; it is empty when no redirect occurred. Check that the navigation response is not null before reading it.
const response = await page.goto('http://example.com');
if (response) {
const chain = response.request().redirectChain();
console.log('Redirect count:', chain.length);
console.log('Redirected from:', chain.map(request => request.url()));
}
page.goto() resolves with the response for the last redirect, not a list of all redirect responses. Its request’s redirectChain() gives you the earlier requests. For example, an HTTP-to-HTTPS redirect includes the original HTTP request in the chain. See the Puppeteer redirectChain API and Page.goto API.
1. Run a complete redirect trace
Install Puppeteer in a Node.js project, save this as trace-redirects.js, and run it with a URL argument. Puppeteer’s package includes a compatible browser download as part of its normal installation workflow.
npm install puppeteer
// trace-redirects.js
const puppeteer = require('puppeteer');
async function main() {
const url = process.argv[2] || 'http://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
if (!response) {
console.log('Navigation produced no HTTP response.');
return;
}
const request = response.request();
const chain = request.redirectChain();
console.log('Requested URL:', url);
console.log('Final URL:', response.url());
console.log('Final status:', response.status());
console.log('Redirect count:', chain.length);
console.log('Redirect chain:');
for (const [index, redirectedRequest] of chain.entries()) {
console.log(`${index + 1}. ${redirectedRequest.method()} ${redirectedRequest.url()}`);
}
console.log(`${chain.length + 1}. ${request.method()} ${request.url()} (final request)`);
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
node trace-redirects.js http://example.com
The chain lists predecessor requests. The final request is the request attached to the final response, so the example prints it separately. A zero-length chain means no redirect was recorded for that request.
2. Log requests and responses as they happen
Use page events when you need an ongoing log, want to see subresource requests, or need to correlate a redirect with surrounding network activity. Puppeteer emits request and response events by default.
const page = await browser.newPage();
page.on('request', request => {
console.log('request', request.method(), request.url());
});
page.on('response', response => {
console.log('response', response.status(), response.url());
});
page.on('requestfailed', request => {
console.log('failed', request.url(), request.failure()?.errorText);
});
const response = await page.goto(url);
if (response) {
const chain = response.request().redirectChain();
console.log('redirect predecessors', chain.map(request => request.url()));
}
A page loads more than its top-level document. To focus the event log on navigation requests, filter on request.isNavigationRequest():
page.on('request', request => {
if (request.isNavigationRequest()) {
console.log('navigation request', request.method(), request.url());
}
});
page.on('response', response => {
if (response.request().isNavigationRequest()) {
console.log('navigation response', response.status(), response.url());
}
});
A redirect completes the original request and causes a new request. It is not inherently a request failure. A redirect response’s request can emit requestfinished, followed by a new request. The API describes this lifecycle in the HTTPRequest reference; the network logging guide shows request and response event listeners.
3. Choose the right tracing method
| Need | Use |
|---|---|
| List redirect predecessor URLs for one completed navigation | response.request().redirectChain() |
| Observe traffic while the page loads | request, response, and optionally requestfailed events |
| See request initiators visually | Chrome DevTools Network panel |
| Inspect Chrome protocol events for a specific advanced need | Puppeteer CDPSession |
The DevTools Network panel records requests while it is open and labels HTTP redirects as a “Redirect” initiator. It is a useful visual cross-check; see the Chrome DevTools Network reference. Puppeteer also exposes a CDPSession for raw Chrome DevTools Protocol commands, but the standard redirect-chain API is sufficient for ordinary tracing. The HTTPRequest documentation warns that directly accessing its client can break Puppeteer, so use raw protocol access only when the higher-level API does not cover a concrete need.
4. Interpret the results correctly
- The chain is empty: no redirect predecessor was recorded for that request.
- The chain has entries: those entries are the preceding requests in the resource chain. The response returned by
page.goto()belongs to the last request. - The final status is 404 or 503: this is still an HTTP response. Inspect
response.status(); it does not by itself mean Puppeteer emittedrequestfailed. - A request failed: transport problems, timeouts, and similar failures are reported through
requestfailed. Checkrequest.failure()?.errorText. - The response is null: some navigations do not produce a navigation response, including navigation to
about:blankand same-URL navigation with a different hash.
This distinction helps avoid treating an HTTP error status as a network failure. For the documented request lifecycle and navigation behavior, consult the HTTPRequest API and Page.goto API.
5. Navigation options and practical choices
The redirect chain itself does not need a special option. The main choice is how long navigation should wait before page.goto() resolves:
waitUntil: 'load'waits for the load event.waitUntil: 'domcontentloaded'waits until the initial HTML has been parsed, often a useful point for tracing the document redirect.waitUntil: 'networkidle0'or'networkidle2'waits for low network activity. Pages with long-lived or recurring requests may not reach this condition promptly.timeoutsets the navigation timeout in milliseconds. Choose it for your environment and handle timeout errors explicitly.
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
These options control when navigation returns or times out; they do not change what redirectChain() means. The Page.goto reference documents the supported navigation options and special cases.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Cannot read properties of null while reading the chain |
page.goto() returned null for a navigation that has no HTTP response. |
Check if (response) before calling response.request(). |
| The chain is empty, but the final URL differs from the input | The URL may have been normalized client-side, or the observed change may not be an HTTP redirect in that request chain. | Log navigation request and response events; compare the final URL and inspect DevTools Network initiators. |
There is a 404 or 503, but no requestfailed event |
HTTP error statuses are completed responses, not transport failures. | Inspect response.status() and handle status codes as application-level results. |
| Navigation times out on a page that appears to have loaded | The selected wait condition may depend on activity that continues, such as recurring network requests. | Try domcontentloaded when the document and redirect trace are what you need; set a suitable timeout. |
| The event log contains many unrelated URLs | Pages request scripts, images, fonts, and other resources in addition to the document. | Filter with request.isNavigationRequest() for navigation-only output. |
| Expected request or response events are missing | Listeners may have been attached after navigation started, or logging may target another page. | Register listeners on the correct page before calling page.goto(). |
7. Performance, reliability, and cost
Reading redirectChain() after navigation is a small inspection step; most runtime is spent launching the browser and loading the target site. Reuse a browser process for multiple independent pages when appropriate, close pages and browsers when finished, and choose a wait condition that matches the evidence you need. A short navigation wait can capture the document redirect trace without waiting for every later resource, while event listeners provide ongoing visibility.
For reliable diagnostics, record the input URL, final response URL, status, chain URLs, and any request failure text. Treat HTTP status and request failure as separate signals. Site behavior can vary by time, cookies, geography, and anti-bot checks, so preserve enough context to reproduce a trace. Puppeteer is an open-source browser automation library; the cited API documents behavior but does not establish a universal runtime or cost figure. Browser compute and the target site’s response time determine your operational cost.
8. Or skip the browser setup
If you need a clean screenshot of a page after reaching its destination, ScreenshotNeo is a website screenshot API and MCP server. It returns an image or PDF from one GET request; it is useful for capture, while Puppeteer’s redirect chain remains the way to inspect each navigation predecessor programmatically.
See the ScreenshotNeo API documentation for the request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);
- Cookie banners are accepted and removed before capture; more than 60 known consent platforms, newsletter popups, and chat widgets can be removed, with each step configurable.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Responses identify the page verdict and billing status in headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents and MCP clients. - The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan.
Sign up for 1,000 free screenshots a month, with no card required.
9. FAQ
Does redirectChain() include the final URL?
No. It returns preceding requests. The final request is available from the response returned by page.goto().
Does an empty chain prove that no URL changed?
It means no predecessor request was recorded in that resource chain. Use request and response events or the DevTools Network panel to investigate URL changes outside that chain.
Can I use the chain for images and scripts too?
The chain belongs to an HTTPRequest, so it can be inspected for other resource requests as well. For a page-wide trace, listen to network events and filter by resource or navigation as needed.
Should I use the raw Chrome DevTools Protocol?
Usually no. Use Puppeteer’s chain and page events for normal tracing. Reach for CDPSession only when you need protocol-level data those APIs do not expose.
Documentation version note: the research sources identify Puppeteer 25.12.0 for the HTTPRequest, Page.goto, and network logging pages; the standalone redirectChain page identifies 25.10.0. Check the documentation matching the version installed in your project.


