QA How-To
How to Use Playwright route abort (2026)
Learn Playwright route abort to block resources, simulate network failures, assert request errors, scope handlers correctly, and keep tests reliable in 2026.
20 min read | 2,767 words
TL;DR
Register a route with `page.route()` or `browserContext.route()`, match the request you want to fail, and call `await route.abort()` or `await route.abort('connectionrefused')`. Set the route before navigation or the triggering action, and assert the `requestfailed` event plus the application's recovery UI.
Key Takeaways
- Register a page or context route before the action that sends the request.
- Call `route.abort()` only for the intended request and continue or fall back for everything else.
- Use documented abort error codes to model specific transport failures without inventing values.
- Assert both the failed request and the user-visible recovery behavior.
- Prefer browser-context routes when popups and newly opened pages must share the policy.
- Remove broad routes after the scenario so later steps do not inherit hidden network changes.
- Block service workers when they make requests invisible to native page and context routing.
Playwright route abort intentionally stops a browser request before it reaches the network. Register a handler with page.route() or browserContext.route(), identify the request, and call route.abort() to test blocked assets, disconnected services, DNS failures, timeouts, and application recovery behavior.
The API is simple, but reliable network-failure tests require careful matching, timing, assertions, and cleanup. This guide uses current Playwright Test TypeScript APIs and explains where abort fits beside continue, fallback, and fulfill.
TL;DR
import { test, expect } from '@playwright/test';
test('shows an error when orders cannot load', async ({ page }) => {
await page.route('**/api/orders', async route => {
await route.abort('connectionrefused');
});
const failedRequest = page.waitForEvent(
'requestfailed',
request => request.url().includes('/api/orders'),
);
await page.goto('https://example.com');
await page.evaluate(() => fetch('/api/orders').catch(() => undefined));
const request = await failedRequest;
expect(request.failure()?.errorText).toBeTruthy();
});
| Route action | Browser request outcome | Best use |
|---|---|---|
route.abort() |
Fails before a response | Transport and offline-style failure tests |
route.continue() |
Goes to the network, optional request overrides | Pass through or request mutation |
route.fallback() |
Continues to the next matching handler | Layered route policies |
route.fulfill() |
Receives a synthetic response | API response mocking |
route.fetch() then fulfill() |
Receives a modified real response | Response patching |
1. What Playwright Route Abort Does
A Playwright Route object exists when a request matches a handler registered through page.route or browserContext.route. The handler must resolve that route exactly once. Calling route.abort() marks the request as failed. The browser does not receive an HTTP status because an abort represents a network-level failure, not a server response such as 404 or 503.
That distinction shapes the test. An application may handle a 503 response through normal error-response logic while treating a transport failure as offline behavior. If the acceptance criterion says the server returns 503, use route.fulfill({ status: 503 }). If the criterion says the connection drops, DNS fails, or the client blocks the request, abort is the closer model.
Without an argument, route.abort() uses the generic failed error code. Playwright also accepts a documented set of codes such as aborted, accessdenied, addressunreachable, blockedbyclient, blockedbyresponse, connectionaborted, connectionclosed, connectionfailed, connectionrefused, connectionreset, internetdisconnected, namenotresolved, timedout, and failed. These are controlled browser-facing failure categories. They do not reproduce every packet-level detail of a real network incident.
Abort operates only on matching requests observed after route registration. It does not retroactively cancel a completed request. Register the handler before page.goto, a click, or JavaScript that initiates the call. Use a precise matcher so unrelated scripts, documents, and APIs still load.
2. Create a Runnable First Abort Test
The following test blocks image requests while loading the Playwright documentation site. It compiles and runs in a standard Playwright Test project. The assertion checks page behavior that does not depend on images, and the counter proves that the handler actually aborted at least one matching resource.
// tests/block-images.spec.ts
import { test, expect } from '@playwright/test';
test('loads documentation content while image requests are blocked', async ({ page }) => {
let abortedImages = 0;
await page.route('**/*', async route => {
const request = route.request();
if (request.resourceType() === 'image') {
abortedImages += 1;
await route.abort('blockedbyclient');
return;
}
await route.continue();
});
await page.goto('https://playwright.dev');
await page.evaluate(() => new Promise<void>(resolve => {
const image = new Image();
image.addEventListener('error', () => resolve(), { once: true });
image.src = '/qa-aborted.png';
document.body.append(image);
}));
await expect(page).toHaveTitle(/Playwright/);
expect(abortedImages).toBeGreaterThan(0);
});
Run it with npx playwright test tests/block-images.spec.ts --project=chromium. The broad **/* matcher is safe here only because the callback has a clear branch for every request. Image requests abort, and every other request continues. Forgetting the pass-through branch leaves requests unresolved until an error or timeout.
For pure resource blocking, a narrower glob is shorter: await page.route('**/*.{png,jpg,jpeg}', route => route.abort()). The resource-type approach also catches formats and image delivery URLs without recognizable file extensions. It can block SVG images when the browser classifies them as image resources, while a file-extension glob may not. Choose based on what the scenario means, not on code length.
Do not use this optimization in functional tests whose behavior depends on lazy images, layout shifts, image load callbacks, canvases, or visual assertions. Network policy is part of the test environment and can change product behavior.
3. Match Requests Precisely With Glob, RegExp, or Predicate Logic
Playwright route registration accepts a glob string, regular expression, or function that evaluates a URL. Glob patterns must match the entire URL. A single * does not cross /, while ** can. Curly braces provide alternatives, as in **/*.{png,jpg,jpeg}. For query-aware or domain-sensitive policies, a URL predicate is often easiest to review.
import { test, expect } from '@playwright/test';
test('blocks only analytics collection', async ({ page }) => {
const blockedHosts = new Set([
'analytics.example.test',
'metrics.example.test',
]);
await page.route(
url => blockedHosts.has(url.hostname) && url.pathname.startsWith('/collect'),
route => route.abort('blockedbyclient'),
);
await page.goto('https://example.com');
await expect(page.locator('h1')).toContainText('Example Domain');
});
A route URL predicate receives a URL, which avoids fragile substring checks. url.hostname === 'analytics.example.test' cannot accidentally match analytics.example.test.attacker.invalid. Path checks remain visible and testable.
When one broad handler inspects route.request(), consider method and resource type too. An endpoint may receive both an OPTIONS preflight and a POST. Aborting the preflight can prevent the POST, which may be correct for a browser-level failure but wrong when the scenario specifically requires the POST to fail. Similarly, an API path can be loaded as fetch or navigation.
Log matching information temporarily during diagnosis: method, resource type, and URL. Remove noisy logs after stabilizing the matcher, or attach a small structured record only on failure. If route interception seems unrelated to a failing locator, Playwright locator waiting diagnostics can help distinguish a missing network response from an element-state problem.
4. Abort by Resource Type Without Breaking Navigation
request.resourceType() reports categories such as document, stylesheet, image, media, font, script, texttrack, xhr, fetch, eventsource, websocket, manifest, and other, depending on the browser and request. Resource-type filtering is useful, but do not assume every backend call is xhr. Modern applications commonly use fetch.
const blockedTypes = new Set(['image', 'media', 'font']);
await page.route('**/*', async route => {
const type = route.request().resourceType();
if (blockedTypes.has(type)) {
await route.abort('blockedbyclient');
return;
}
await route.continue();
});
Blocking document would abort a matching navigation and cause page.goto to reject. Blocking script can leave the page visibly present but functionally inert. Blocking stylesheets can change locator visibility, element size, responsive breakpoints, and screenshot output. Use these effects intentionally in resilience or performance experiments, not as a default speed tweak across the suite.
A safer optimization targets known third-party resources instead of whole categories. Product scripts, fonts, and images may carry behavior. An analytics host, ad host, or optional video CDN has a clearer contract. Even then, verify the application does not wait indefinitely for a callback from the blocked vendor.
If the goal is to prove that core content works without optional resources, assert both sides: the optional request failed and the core user action still succeeds. A faster run is not itself a functional assertion. Record why the handler exists so a future maintainer does not remove it as unexplained test plumbing.
5. Simulate Network Failures With Playwright Route Abort Error Codes
The error-code argument lets a test communicate the intended class of failure. Use only documented strings. Playwright defaults to failed, which is sufficient when the UI should handle all transport errors the same way. Choose a specific code when the application, logging, or requirement differentiates offline, DNS, refusal, reset, or timeout-like failures.
| Error code | Scenario represented | Example test purpose |
|---|---|---|
connectionrefused |
Target refused a connection | API process is unavailable |
connectionreset |
Connection reset | Mid-transport resilience path |
internetdisconnected |
Internet connection lost | Offline banner or queued action |
namenotresolved |
Host name not resolved | DNS failure handling |
timedout |
Operation timed out | Client timeout recovery branch |
blockedbyclient |
Client blocked the request | Optional vendor or ad blocking |
failed |
Generic failure | Common fallback behavior |
import { test, expect } from '@playwright/test';
test('records a refused recommendations request', async ({ page }) => {
await page.goto('https://example.com');
await page.route('**/api/recommendations', route =>
route.abort('connectionrefused'),
);
const failure = page.waitForEvent(
'requestfailed',
request => request.url().endsWith('/api/recommendations'),
);
const fetchResult = await page.evaluate(async () => {
try {
await fetch('/api/recommendations');
return 'unexpected success';
} catch {
return 'network failure';
}
});
expect(fetchResult).toBe('network failure');
expect((await failure).failure()).not.toBeNull();
});
An abort code is a simulation label, not a guarantee that every browser exposes identical human-readable error text. Assert the documented test outcome and application behavior rather than a long browser-specific string.
6. Choose Page Route or Browser Context Route
page.route scopes interception to one page. browserContext.route applies across pages in that context, including popups and new tabs. Context routing is the better default for a policy that must hold throughout a multi-page flow. Page routing is safer when one test page needs a temporary failure and sibling pages should remain untouched.
import { test, expect } from '@playwright/test';
test('blocks telemetry in the page and popup', async ({ context, page }) => {
let blocked = 0;
await context.route('**/telemetry/**', async route => {
blocked += 1;
await route.abort('blockedbyclient');
});
await page.goto('https://example.com');
await page.evaluate(() => fetch('/telemetry/page-view').catch(() => undefined));
const popup = await context.newPage();
await popup.goto('https://example.com');
await popup.evaluate(() => fetch('/telemetry/popup-view').catch(() => undefined));
expect(blocked).toBe(2);
});
When page and context routes both match, page-level routing takes precedence. Multiple matching handlers add another ordering rule: handlers run in reverse registration order when they delegate with fallback. A handler that calls continue sends the request immediately and prevents earlier matching handlers from running. A handler that calls abort terminates it.
Keep the smallest safe scope. Context-wide **/* handlers are powerful and easy to forget. Playwright creates a fresh context for each standard test by default, which limits leakage between tests, but leakage can still occur between steps in the same test or when custom fixtures reuse contexts.
7. Assert Request Failure and User Recovery
A network test is incomplete when it merely installs a route. Prove that the intended request occurred and failed, then prove the application response. requestfailed fires for requests that fail at the network level. An HTTP 404 or 503 still has a response and does not count as requestfailed.
import { test, expect } from '@playwright/test';
test('offers retry after a transport failure', async ({ page }) => {
let attempts = 0;
await page.route('**/api/orders', async route => {
attempts += 1;
if (attempts === 1) {
await route.abort('internetdisconnected');
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 101, status: 'ready' }]),
});
});
await page.goto('https://example.com');
const firstFailure = page.waitForEvent(
'requestfailed',
request => request.url().endsWith('/api/orders'),
);
await page.evaluate(() => fetch('/api/orders').catch(() => undefined));
await firstFailure;
const result = await page.evaluate(async () => {
const response = await fetch('/api/orders');
return response.json();
});
expect(result).toEqual([{ id: 101, status: 'ready' }]);
expect(attempts).toBe(2);
});
In a real application, trigger both calls through user actions and assert a visible error, retry control, preserved form data, accessible status announcement, and successful recovery. The self-contained example focuses on routing mechanics. Avoid waitForTimeout to guess when the request occurred. Start an event or response promise before the triggering action and await it.
Use request.failure()?.errorText for diagnostic evidence, but keep assertions tolerant of browser-specific wording. The chosen abort code and visible product behavior are more stable than a low-level message.
8. Control Handler Order and Clean Up Routes
Every matching route must end in one terminal or delegating action: abort, continue, fulfill, or fallback. Do not call two. Return after a terminal branch so later code cannot accidentally touch the handled route.
Use fallback when several policies should compose. Handlers run in reverse order of registration. The last registered handler sees the request first and may modify or delegate it to the previously registered handler. continue is different because it immediately sends the request to the network and skips remaining matching handlers.
await page.route('**/*', route => route.continue());
await page.route('**/api/**', async route => {
if (route.request().method() === 'DELETE') {
await route.abort('accessdenied');
return;
}
await route.fallback();
});
The API handler is registered last, so it runs first. It aborts DELETE calls and falls back for other API calls. The earlier catch-all then continues those requests. This layered style is useful in shared fixtures, but excessive stacking makes behavior difficult to trace. Prefer one handler when the policy is local and simple.
Remove temporary handlers with page.unroute or context.unroute when the same page or context continues into a phase that expects normal networking. Supply the same URL pattern, and optionally the same handler reference, to remove only what you installed. Standard Playwright Test page fixtures are discarded after each test, but explicit cleanup documents the boundary inside long workflows.
9. Handle Service Workers, Cache, and Invisible Requests
A service worker can intercept application requests before Playwright's native page or context route sees them. If a request expected by the test never reaches the handler, configure the browser context with serviceWorkers: 'block'. This is especially relevant when the application uses a service-worker mocking library or an offline cache.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
serviceWorkers: 'block',
},
});
Blocking service workers changes the application environment. It is appropriate when the goal is to use Playwright's native routing deterministically. It is inappropriate when service-worker behavior itself is under test. In that case, design the test around the actual worker boundary and use the relevant worker-routing support rather than pretending the worker is absent.
Routing can also alter caching behavior, so do not use a routed performance test to make claims about real repeat-visit cache performance. Network interception is a functional control mechanism. Treat timings gathered under it as diagnostic, not production performance evidence.
Popups add a timing wrinkle. A page-specific route may not intercept the first request of a popup before code obtains the new page. A context route registered before the action covers that initial navigation. Choose context scope when the very first popup document or API request must be blocked.
10. Debug Playwright Route Abort Failures in CI
When a route never fires, inspect the complete URL, method, and resource type. Confirm registration happens before the action. Check whether the request is served by a service worker, whether a popup uses another page, and whether a glob matches the full URL. Replace a fragile glob temporarily with **/* plus logging, then narrow it once the real request shape is known.
When the wrong request aborts, examine redirects, preflight requests, query parameters, and host boundaries. A substring matcher for /api/user may also match /api/users, /api/user-settings, or an unrelated host. URL predicates make host and path equality explicit.
When page.goto fails unexpectedly, verify that the handler did not abort document. A broad resource-type set or **/* handler can terminate the main navigation. When the application hangs, ensure every branch awaits and returns an action.
Retain traces on first retry, screenshots on failure, and concise request logs. A trace helps correlate the user action, page state, and network events, but a deliberate abort will appear as a failed request by design. The assertion must distinguish the expected injected failure from an unrelated outage. If the target itself is unreachable before routing, consult fixing Playwright net::ERR_CONNECTION_REFUSED.
CI tests should avoid third-party availability as a prerequisite. Use a controlled test application or synthetic page for routing mechanics, and reserve real integration checks for environments where the dependency contract is the subject of the test.
Interview Questions and Answers
Q: What does route.abort() do in Playwright?
It fails a matched browser request before a response is delivered. By default it uses a generic failure code, and it can accept documented codes such as connectionrefused or internetdisconnected. It models transport failure, not an HTTP error response.
Q: What is the difference between aborting and fulfilling with status 500?
Abort produces a network-level failure and triggers requestfailed. Fulfilling with status 500 produces a valid HTTP response whose status is 500. Applications often handle those paths differently, so the test should choose the mechanism that matches the requirement.
Q: Why must a route be registered before the action?
Routing affects future matching requests. If navigation or a click sends the request first, the handler cannot retroactively intercept it. I register the route, start any event promise, and then trigger the user action.
Q: When would you use browserContext.route instead of page.route?
I use a context route when the policy must cover multiple pages, popups, or the initial request of a newly opened page. I use a page route for a narrow scenario on one page. Smaller scope reduces accidental effects.
Q: How do multiple route handlers interact?
Matching handlers can form a reverse-registration chain when they call fallback. continue sends the request immediately and skips other matching handlers. abort and fulfill also terminate handling, so every route should take exactly one path.
Q: How do you assert that an abort worked?
I await a matching requestfailed event and assert the user-visible recovery behavior. I may inspect request.failure() for diagnostics, but I avoid exact browser-specific error-text assertions unless the product contract requires them.
Q: Why might a route not see a request?
The matcher may be wrong or registered late, the request may be on another page, or a service worker may intercept it first. I log the actual URL and resource type, consider context scope, and block service workers only when native routing is the intended test environment.
Q: Is aborting images a good way to speed every test?
Not automatically. Images can affect layout, callbacks, lazy loading, screenshots, and user interactions. I block them only where the test contract does not depend on those behaviors and verify that the policy does not hide defects.
Common Mistakes
- Registering the route after navigation or after the click that sends the target request.
- Using
route.abort()to model an HTTP 404 or 503 instead of fulfilling a response with that status. - Matching
**/*and forgetting to continue or fall back for requests that should proceed. - Aborting the
documentresource unintentionally and then treating a rejectedpage.gotoas an application defect. - Asserting only that a handler ran, without checking the failed request and recovery UI.
- Depending on exact low-level error text across Chromium, Firefox, and WebKit.
- Installing a page route when the target request originates in a popup or sibling page.
- Ignoring service workers when expected requests never reach native routing.
- Leaving a broad route active during later phases that require real networking.
- Blocking resources globally for speed without checking functional and visual side effects.
Conclusion
Use Playwright route abort when the behavior under test requires a request to fail at the network layer. Match the smallest useful request set, register the handler before the trigger, choose a documented error code only when it adds meaning, and let every non-target request proceed.
A credible resilience test proves more than interception. It observes the failed request, checks the user-facing fallback, exercises recovery, and cleans up the injected fault. Start with one controlled endpoint, then expand the pattern to context scope or layered handlers only when the workflow requires it.
Interview Questions and Answers
Explain Playwright route abort and a realistic use case.
`route.abort()` terminates a matched browser request as a network failure. I use it to verify that an application shows an error, preserves user state, and offers recovery when a critical API is unreachable. I register the route before the action and assert both `requestfailed` and the visible fallback.
How is `route.abort()` different from `route.fulfill()`?
Abort creates no HTTP response and represents a transport failure. Fulfill supplies a synthetic response, including status, headers, and body. I use abort for offline or connection scenarios and fulfill for server response scenarios such as 404, 429, or 503.
What happens to requests that a broad route should not abort?
The handler must resolve them with `route.continue()` or delegate with `route.fallback()`. Every matched route needs exactly one handling path. I return after terminal branches to prevent accidental double handling.
Which scope would you choose for popup network failures?
I register `browserContext.route()` before the action that opens the popup. It can cover the popup's initial request and later calls, while a page route is limited to its page. I keep context-wide patterns as narrow as possible.
How do you avoid flaky route-abort assertions?
I start a predicate-based `requestfailed` promise before triggering the action and await it instead of sleeping. I match a precise host, path, and sometimes method. The main assertion is deterministic recovery UI, not a browser-specific error string.
What does `route.abort('timedout')` simulate?
It labels the failed route as a timeout-class browser failure. It does not reproduce the passage of a real server timeout or every network timing detail. If elapsed-time behavior matters, I design a controlled delayed service in addition to checking the timeout recovery branch.
How do service workers affect Playwright routing?
A service worker can handle a request before native page or context routing sees it. For tests whose contract is Playwright-native interception, I can create contexts with service workers blocked. If the worker itself is under test, I keep it enabled and test that boundary deliberately.
Would you abort all images to speed a regression suite?
Only after proving those tests do not depend on images, layout, lazy loading, callbacks, or visual behavior. A blanket optimization changes the environment and can hide defects. I prefer targeted optional third-party blocking and document the reason.
Frequently Asked Questions
How do I abort a request in Playwright?
Register a matching handler with `page.route()` or `browserContext.route()`, then call `await route.abort()`. The handler must be installed before the action that triggers the request.
Can Playwright abort only image requests?
Yes. Match image file extensions or inspect `route.request().resourceType()` and abort when it equals `image`. Continue every non-image request so the page can load normally.
What error code does route.abort use by default?
The default is `failed`, a generic network failure. You can pass a documented code such as `connectionrefused`, `internetdisconnected`, `namenotresolved`, or `timedout` when the scenario needs a more specific category.
Does route.abort return an HTTP status code?
No. It fails the request at the network layer, so there is no HTTP response. Use `route.fulfill({ status: 500 })` when the test needs a server response with an error status.
How can I verify an aborted request in Playwright?
Wait for a matching `requestfailed` event and inspect `request.failure()` for diagnostic information. Also assert the application's visible error, retry, or offline behavior.
Why is page.route not intercepting my request?
Common causes are a late registration, an incorrect full-URL glob, a request from another page, or a service worker intercepting first. Log the actual request URL and consider a context route or `serviceWorkers: 'block'` when appropriate.
How do I remove an abort route?
Call `page.unroute()` or `browserContext.unroute()` with the registered URL pattern, optionally including the same handler reference. Removing temporary handlers is important when later steps need normal networking in the same page or context.