Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To detect every redirect in Playwright, attach a response listener before navigation, then start from the response returned by page.goto() and follow its request’s redirectedFrom() links backward. page.goto() resolves with the last redirect response, not a list of every hop. To control redirects, use routing; to cap automatic following while inspecting a response, use route.fetch({ maxRedirects }).

What Playwright does when a server redirects

A server redirect ends the current request. Playwright creates a new Request for the next URL, and navigation continues. The lifecycle for a request includes request, response, and requestfinished events. A redirect is therefore a chain of distinct requests, not one request whose URL changes.

This distinction explains why looking only at page.url() or the value returned from page.goto() cannot reveal all intermediate destinations. The page URL tells you where navigation ended; the request chain tells you how it got there.

What the navigation response contains

When navigation completes with an HTTP response, page.goto() returns the last redirect response. Its request() gives you the final request in the chain. From there, redirectedFrom() steps back through each earlier request, until it returns null. Each request exposes its URL, so you can reconstruct the chain in order.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The reverse link redirectedTo() points toward the next request. Use it if you start with an earlier request and want to walk forward; use redirectedFrom() when you start with the final navigation response.

Capture and assert the full redirect chain

Register listeners before calling page.goto(), so the earliest response events are not missed. The following TypeScript example runs as a Node.js script, records response URLs and statuses, reconstructs the navigation chain, and checks the final destination.

import { chromium } from 'playwright';

const startUrl = process.argv[2] ?? 'https://example.com';
const expectedFinalUrl = process.argv[3];

async function main() {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  const observedResponses: Array<{ url: string; status: number }> = [];

  // Register before navigation so early redirect responses are observed.
  page.on('response', response => {
    observedResponses.push({
      url: response.url(),
      status: response.status(),
    });
  });

  try {
    const finalResponse = await page.goto(startUrl);
    if (!finalResponse) {
      throw new Error('Navigation produced no response');
    }

    const chain: string[] = [];
    for (
      let request = finalResponse.request();
      request;
      request = request.redirectedFrom()
    ) {
      chain.unshift(request.url());
    }

    const result = {
      chain,
      finalUrl: finalResponse.url(),
      status: finalResponse.status(),
      observedResponses,
    };
    console.log(JSON.stringify(result, null, 2));

    if (expectedFinalUrl && finalResponse.url() !== expectedFinalUrl) {
      throw new Error(
        `Unexpected final URL: ${finalResponse.url()}`
      );
    }
  } finally {
    await browser.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Run it with a starting URL and, optionally, the destination you expect:

node detect-redirects.js https://example.com/start https://example.com/final

The chain array is ordered from the starting URL through the final request. observedResponses records response events and their statuses as they arrive. Treat it as an event log, not as the canonical chain: a page can make requests beyond its main navigation, while the chain reconstructed from finalResponse.request() specifically follows the navigation response’s redirect ancestry.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

Choose assertions that match the behavior you need

  • Final destination: Assert finalResponse.url() or, after navigation, page.url() against the expected canonical URL.
  • Every hop: Assert the complete array, not just its first and last entries. This catches unexpected intermediate hosts, path changes, and scheme changes.
  • Status contract: Record response statuses and assert the redirect and final statuses your application is supposed to return. Do not assume a particular redirect count.
  • Timing: Attach response listeners before navigation. Adding them after page.goto() can miss earlier responses.

Intercept, rewrite, or block a request

Use page.route() or browserContext.route() when the test needs to control traffic rather than only observe it. A route handler can continue ordinary traffic, abort an unwanted request, or fulfill a response with test data.

await page.route('**/account', async route => {
  await route.continue();
});

await page.route('**/unwanted', async route => {
  await route.abort();
});

await page.route('**/test-data', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ ok: true }),
  });
});

Routing treats a request and its redirects as one unit. In the usual redirect flow, the route handler sees the original request while the browser follows the chain; it does not provide a fresh interception point for every hop. This matters when designing a test: use the request-chain links or response events to observe hops, and use routing to decide what happens to the intercepted request.

To send a request to another URL, pass the replacement URL to route.continue() or route.fallback(). To stop an unwanted request, abort it. To supply known test content, fulfill it directly. These approaches serve different purposes: rewriting sends traffic elsewhere, aborting prevents it from proceeding, and fulfilling returns a test response.

Inspect a response with route.fetch()

route.fetch() makes the request and returns a response that the handler can inspect or modify before fulfilling the browser’s request. It follows redirects automatically, unless the configured maxRedirects limit is exceeded. In that case, the call throws, which gives tests a way to fail predictably on excessive redirect depth.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.route('**/login', async route => {
  const response = await route.fetch({ maxRedirects: 5 });
  const body = await response.text();
  await route.fulfill({ response, body });
});

Choose a limit based on the expected behavior in your test. The option is a cap, not a promise that a site will redirect a particular number of times; there is no need to assume a fixed default. If the actual chain exceeds your configured limit, handle the thrown error as the test outcome rather than expecting a response object.

Do not fulfill a 3xx response to trigger another route pass

A fulfilled redirect response is not a portable way to make Playwright re-enter the route handler for the destination. Chromium and Firefox follow a fulfilled 3xx redirect without calling the handler again; WebKit rejects that call. If the test needs alternate content, fulfill that content directly. If it needs the request sent to another location, use route.continue() or route.fallback() with a URL instead.

Common failures and how to diagnose them

The chain contains only the final URL

Start traversal from finalResponse.request() and repeatedly call redirectedFrom(); do not expect page.goto() itself to return every response. Also make sure the listener was registered before navigation if you are relying on events to record early response statuses.

Navigation has no response object

Check for the explicit null case before calling request(). A navigation that produces no response cannot supply the response-backed chain shown above, so report that condition separately instead of treating it as an empty redirect chain.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

A 404 or 503 is mistaken for a network failure

HTTP error statuses still complete as HTTP responses; they are not, by themselves, requestfailed network errors. Inspect the response status when the server answered, and reserve network-failure handling for requests that actually fail at the network level.

route.fetch() throws while following a chain

Check whether the redirect sequence exceeds the configured maxRedirects. If it does, the fetch throws by design. Set a limit that allows the redirects the application legitimately requires, while retaining a finite cap when the test must detect a loop or unexpectedly deep chain.

A route handler does not see a redirect destination

That is consistent with routing’s request-and-redirect unit behavior. Do not rely on the handler being invoked again for every hop or for a fulfilled 3xx response. Use response events and request-chain traversal for observation, or rewrite the original request to the destination when the test requires routing control.

Routing changes caching or misses a service-worker request

Routing can disable the HTTP cache, and requests intercepted by service workers may bypass page.route(). If either condition affects the test, consider context-level routing or blocking service workers for that setup. Verify the behavior in the same browser and context configuration used by the test rather than assuming a page route will observe every request.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and test reliability

For a navigation assertion, reconstructing the chain after page.goto() is direct and avoids treating unrelated response events as redirects. Add event listeners when you need statuses captured as responses arrive; keep their recorded data scoped to the navigation under test if the page also makes background requests.

Use a finite maxRedirects when fetching through routes and when redirect loops or unbounded chains are a test concern. Avoid using mocked 3xx fulfillment as a cross-browser redirect mechanism, because its behavior differs between Chromium, Firefox, and WebKit. Finally, distinguish an HTTP response with an error status from a network failure: conflating the two can make a test pass or fail for the wrong reason.

Redirect behavior can also depend on the requested URL, response headers, cookies, and application state. Make assertions about the chain and destination that form part of the application contract, rather than assuming one universal URL sequence for every run.

Or skip the browser setup

ScreenshotNeo is a website screenshot API, not a replacement for Playwright’s redirect-chain assertions. If you also need a clean screenshot of a URL, one GET request can return an image or PDF; the API and available options are in the ScreenshotNeo documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are not billed. Its MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The Free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. Learn more at ScreenshotNeo, or sign up free.

Frequently Asked Questions

Can I determine whether the redirect changed only the scheme or host?

Yes. Compare the full URLs in the reconstructed chain, not only their hostnames or paths; the chain includes each request URL.

Should I use response events or redirectedFrom() as my source of truth?

Use the final navigation response’s request chain to establish the navigation hops. Response events are useful for capturing statuses as they arrive, but their log is not limited to that chain.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.