The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use both Playwright’s network events and its redirect links. Attach a response listener before navigation to record every status and URL, then start at the response returned by page.goto() and follow request.redirectedFrom() backward. This lets you verify the complete chain, the final destination, and redirect status codes without guessing which hops occurred.
What Playwright considers a redirect
A server redirect ends the current request. The browser then issues a new request for the location supplied by the server. Playwright exposes the normal network lifecycle for each request: request, response, and requestfinished. The new request is linked to the previous one rather than hidden from the API. The Request API documents this behavior: when a server responds with a redirect, Playwright creates a new Request object.
HTTP errors such as 404 or 503 are still completed HTTP responses; they are not automatically requestfailed network errors. Reserve requestfailed checks for transport failures such as an unavailable host or a connection that ends before a response.
Detect every redirect during navigation
The safest general pattern is to install listeners first, navigate second, and then traverse the final request’s history. The event listener captures status codes as headers arrive; the request chain gives you an ordered list of URLs.
#1 Best Overall
import { test, expect } from '@playwright/test';
test('records the redirect chain and final destination', async ({ page }) => {
const hops: Array<{ url: string; status?: number }> = [];
page.on('response', response => {
const request = response.request();
hops.push({
url: request.url(),
status: response.status(),
});
});
const startUrl = 'https://example.test/old-login';
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());
}
console.log({
chain,
finalUrl: finalResponse.url(),
hops,
});
await expect(page).toHaveURL('https://example.test/login');
expect(finalResponse.url()).toBe('https://example.test/login');
});
page.goto() resolves with the last response in the redirect sequence, not the first 3xx response. Calling finalResponse.url() therefore tells you where navigation ended. Calling finalResponse.request() gives you the last Request object; repeatedly calling redirectedFrom() walks toward the starting request. Because that traversal runs backward, unshift() puts URLs back into chronological order.
Reading the forward link
Every request also has redirectedTo(). It points to the next request and is useful when you begin with an earlier request captured by an event handler. Most tests are simpler with redirectedFrom() from the final navigation response, because it guarantees that you inspect the chain which actually produced the page.
Capture events before navigation
Register page.on('response'), page.on('request'), or page.on('requestfinished') before page.goto(). A listener installed afterward can miss early redirect hops. The documented event order for a request is request, response, then requestfinished.
Assert the chain, destination, and statuses
A useful redirect test checks the contract your application promises instead of merely checking that a page eventually loaded.
Rank #2
- 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
Final destination
const response = await page.goto('https://example.test/legacy');
if (!response) throw new Error('No navigation response');
expect(response.url()).toBe('https://example.test/current');
expect(page.url()).toBe('https://example.test/current');
Use response.url() when you are asserting the navigation response itself; use page.url() when the browser’s current URL is the behavior that matters.
Exact URL chain
const expected = [
'https://example.test/legacy',
'https://example.test/account',
'https://example.test/current',
];
expect(chain).toEqual(expected);
Include scheme and host changes in the expected values. A redirect from HTTP to HTTPS, or from one host to another, is often the security or canonicalization behavior you actually need to protect.
Status-code contract
const redirectStatuses = hops
.filter(hop => hop.status >= 300 && hop.status < 400)
.map(hop => hop.status);
expect(redirectStatuses).toEqual([301, 302]);
expect(hops.at(-1)?.status).toBe(200);
The response event records the status attached to each URL. If your application allows several valid redirect codes, assert membership rather than forcing one code. Keep the final success-status assertion separate so a chain that ends in a 404 cannot pass merely because redirects were present.
Intercept, rewrite, block, or mock redirects
Use page.route() for one page or browserContext.route() for all pages in a context. A handler can continue ordinary traffic, abort unwanted requests, fulfill test data, or send a request to another URL.
Rank #3
await page.route('**/*', async route => {
const request = route.request();
if (request.url().includes('/tracking')) {
await route.abort();
return;
}
await route.continue();
});
Playwright treats a request and its redirects as one routing unit. In normal use the route handler sees the original request while the browser follows the redirect chain. Do not assume that a new handler invocation will occur for every hop.
Inspect or modify the live response with route.fetch()
route.fetch() obtains the response that would have been returned, allowing you to inspect or alter it before fulfilling the route. Redirects are followed automatically unless you set a limit.
await page.route('**/login', async route => {
const response = await route.fetch({ maxRedirects: 5 });
const body = await response.text();
// Inspect, replace, or annotate body as required by the test.
await route.fulfill({ response, body });
});
If the redirect sequence exceeds maxRedirects, route.fetch() throws. This is the practical way to turn an accidental or malicious redirect loop into a predictable test failure. Choose a limit that matches your application’s contract; there is no universal fixed default to rely on.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Rewrite the destination
await page.route('**/legacy', async route => {
await route.continue({ url: 'https://example.test/current' });
});
Use a URL with route.continue() or route.fallback() when you want the request sent elsewhere. Use route.fulfill() when you want to serve a response directly.
Rank #4
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Why fulfilling a 3xx response is not portable
This pattern is tempting but unreliable:
await route.fulfill({
status: 302,
headers: { location: 'https://example.test/next' },
});
Chromium and Firefox follow that fulfilled redirect without entering the route handler again. WebKit rejects the call. Consequently, a test that expects a second interception can pass in one browser and fail in another. If the goal is alternate content, fulfill that content directly. If the goal is to send the request somewhere else, pass a URL to route.continue() or route.fallback().
Stopping redirects and protecting tests
Limit automatic following
Set maxRedirects on route.fetch() when you need a hard depth limit. Catch the thrown error and assert it when testing loop protection:
await page.route('**/loop', async route => {
await expect(route.fetch({ maxRedirects: 3 })).rejects.toThrow();
await route.abort();
});
The exact error text is not a stable cross-browser contract, so assert that the operation fails rather than matching a browser-specific message.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Stop a request entirely
route.abort() prevents the request from reaching the server. This is appropriate for known-bad destinations, tracking endpoints, or a test that verifies a redirect is blocked. It is different from limiting a chain: aborting ends the current request immediately, while maxRedirects permits redirects up to a configured depth and then fails.
Best Value
Service workers and cache
Service-worker-intercepted requests may bypass page.route(). If routing behavior is part of the test, use context routing and consider blocking service workers for that scenario. Routing can also disable the HTTP cache, so do not use a routing test as a performance measurement of normal cached navigation.
Common failures and fixes
| Symptom | Cause | Fix |
|---|---|---|
page.goto() shows only the final URL |
Navigation resolves with the last redirect response. | Start at finalResponse.request() and follow redirectedFrom(). |
| The first hop is missing | Listeners were attached after navigation began. | Register response/request listeners before page.goto(). |
| A 404 is reported as a failed request | HTTP errors are responses, not network failures. | Inspect response status; use requestfailed only for transport errors. |
| Route handler runs once, not once per redirect | Playwright routes the request and its redirects as one unit. | Inspect the chain from the navigation response or use route.fetch(). |
| Fulfilled 302 behaves differently by browser | Chromium and Firefox follow without re-entering; WebKit rejects it. | Fulfill final content directly or use continue({ url })/fallback(). |
| Redirect test hangs or fails unpredictably | Too many hops, a loop, or a service worker bypass. | Set maxRedirects, assert the expected failure, and control service workers. |
Or skip the browser setup
If your goal is a clean image or PDF of a URL rather than browser-level redirect assertions, ScreenshotNeo provides a single HTTP request. It follows the page as a visitor would, accepts cookie or consent banners, and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
Read the parameter and response details in the ScreenshotNeo documentation. A direct cURL call is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The same request in Python:
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)
And in Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
The service includes full-page and element captures, device presets, custom viewport and retina scale, PDF options, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen-TTL caching, signed links, asynchronous webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify a migration.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to try it without a card.
Choosing the right Playwright technique
| Goal | Recommended technique |
|---|---|
| Observe every hop | Response listener plus redirectedFrom() traversal. |
| Verify canonical navigation | Assert finalResponse.url() or page.url(). |
| Block a destination | route.abort(). |
| Send traffic to a test host | route.continue({ url }) or route.fallback(). |
| Inspect or alter a real response | route.fetch(), optionally with maxRedirects, then route.fulfill(). |
| Mock a stable result | Fulfill the final content directly, not a synthetic 3xx chain. |
Frequently Asked Questions
Can I get the first redirect response directly from page.goto()?
No. It resolves with the last response. Record response events or traverse from the final response request with redirectedFrom().
Does a redirect always mean the request failed?
No. A redirect is a normal HTTP response followed by a new request. requestfailed is for network-level failures, not ordinary 3xx, 404, or 503 responses.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsWhich routing scope should I use for redirects in popups or multiple pages?
Use browserContext.route() when the rule must cover every page in the context; use page.route() for one page.
Quick Recap
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.

