Resource library

QA How-To

Playwright route abort: Examples and Best Practices

Use Playwright route abort examples to block assets, fail APIs, test retries, cover popups, compose handlers, and build observable network tests safely.

20 min read | 2,308 words

TL;DR

Use `route.abort()` in focused scenarios: block an optional resource, fail a critical API with a documented error code, or abort only the first attempt to test retry. Await a matching `requestfailed` event, assert the visible recovery behavior, and remove reusable routes when the fault window ends.

Key Takeaways

  • Match the endpoint and condition that represent the risk, not every request on the page.
  • Use an abort for transport failure and a fulfilled response for HTTP status behavior.
  • Install one-shot faults when a scenario needs to prove retry and recovery.
  • Compose shared handlers with `fallback`, because `continue` skips remaining handlers.
  • Use context routing for popup and multi-page policies registered before the opening action.
  • Package repeated fault rules as small helpers with explicit cleanup.
  • Capture request evidence and assert the user outcome instead of relying on sleeps.

These Playwright route abort examples focus on the decisions that make fault-injection tests useful: what to match, which failure to simulate, how long the fault should remain active, and what evidence proves the application recovered. Each pattern uses supported Playwright Test APIs and TypeScript.

Copy the mechanics, then replace example URLs and UI assertions with your application's real contract. A route handler is test infrastructure. The actual test value comes from checking how a user journey behaves when the dependency fails.

TL;DR

await page.route('**/api/orders', route =>
  route.abort('connectionreset'),
);

const failurePromise = page.waitForEvent(
  'requestfailed',
  request => request.url().includes('/api/orders'),
);

await page.getByRole('button', { name: 'Load orders' }).click();
const failedRequest = await failurePromise;

expect(failedRequest.failure()).not.toBeNull();
await expect(page.getByRole('alert')).toContainText('Try again');
Test intention Better control Why
One endpoint is unreachable route.abort() Models missing transport response
Server returns 500 route.fulfill({ status: 500 }) Models an HTTP response
Entire context goes offline context.setOffline(true) Affects all network traffic
Request should change but succeed route.continue({ ... }) Sends an overridden request
Several policies should compose route.fallback() Delegates to another handler

1. Minimal Playwright Route Abort Examples That Actually Run

Begin with a controlled request rather than a complex production page. The test below loads a public document, installs a same-origin API failure, sends the request in browser JavaScript, and observes requestfailed. It does not depend on an application-specific button or selector.

// tests/abort-minimal.spec.ts
import { test, expect } from '@playwright/test';

test('aborts one API request', async ({ page }) => {
  await page.goto('https://example.com');

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

  const failed = page.waitForEvent(
    'requestfailed',
    request => request.url().endsWith('/api/profile'),
  );

  const browserResult = await page.evaluate(async () => {
    try {
      await fetch('/api/profile');
      return 'resolved';
    } catch {
      return 'rejected';
    }
  });

  const request = await failed;
  expect(browserResult).toBe('rejected');
  expect(request.method()).toBe('GET');
  expect(request.failure()).not.toBeNull();
});

Run it with npx playwright test tests/abort-minimal.spec.ts. The page.goto happens before route installation so the main document cannot accidentally match. In a real application test, installing before navigation is often necessary because the API is called during startup. Use a precise path pattern in that case.

The fetch rejects because no response exists. That is the important semantic difference from route.fulfill({ status: 500 }), where fetch resolves to a response whose ok is false. If the product uses a client library, its error objects may differ, but the browser-level distinction remains. For the conceptual foundation and documented error codes, read how to use Playwright route abort.

2. Block Optional Images, Fonts, and Media Selectively

Resource blocking is useful for proving graceful degradation or removing a known optional dependency from a focused functional scenario. Use request.resourceType() when URLs do not have stable extensions. Keep core styles and scripts unless the purpose is specifically to test their absence.

import { test, expect } from '@playwright/test';

test('core content remains available without rich media', async ({ page }) => {
  const blockedTypes = new Set(['image', 'media']);
  const blockedUrls: string[] = [];

  await page.route('**/*', async route => {
    const request = route.request();

    if (blockedTypes.has(request.resourceType())) {
      blockedUrls.push(request.url());
      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-blocked.png';
    document.body.append(image);
  }));

  await expect(page).toHaveTitle(/Playwright/);
  await expect(page.getByRole('heading').first()).toBeVisible();
  expect(blockedUrls.length).toBeGreaterThan(0);
});

A recorded URL list makes the environment change visible. Keep the assertion broad enough that a site can migrate image hosts without making a functional test brittle. For a product suite, the primary assertion should be the critical text, form, or navigation that remains usable.

Fonts deserve caution. Blocking fonts can change line wrapping, layout dimensions, and screenshot results. Media may be the central function of a streaming or learning application. Never promote a local speed experiment to global fixture policy without checking which behaviors it removes.

If file extensions are reliable, a narrow registration avoids inspecting every request: page.route('**/*.{png,jpg,jpeg,webp}', route => route.abort('blockedbyclient')). Glob matching covers the full URL and ** crosses path separators. Query strings and extensionless CDN URLs may require a predicate or resource type instead.

3. Block Third-Party Analytics Without Hiding Product APIs

Third-party calls can add noise or instability to tests, but host matching must be exact. A URL predicate gives access to the parsed hostname and pathname. Avoid url.href.includes('analytics.example'), which can match unintended hosts or query values.

import { test, expect } from '@playwright/test';

const optionalVendors = new Set([
  'analytics.example.test',
  'ads.example.test',
]);

test('blocks optional vendor collection', async ({ page }) => {
  let vendorRequests = 0;

  await page.route(
    url => optionalVendors.has(url.hostname),
    async route => {
      vendorRequests += 1;
      await route.abort('blockedbyclient');
    },
  );

  await page.goto('https://example.com');
  await expect(page.locator('h1')).toHaveText('Example Domain');

  expect(vendorRequests).toBeGreaterThanOrEqual(0);
});

The final count allows zero because the public page does not call those fictional hosts. In an application-specific test where vendor calls are expected, assert a positive count or await a matching requestfailed event. The example demonstrates safe exact-host matching without claiming a third-party request exists.

Decide whether a blocked vendor is part of the scenario contract. If consent management should suppress analytics, aborting analytics does not prove suppression because the application may still attempt the request. In that test, observe requests and assert that none occur. Route abort is appropriate when the browser or test environment intentionally blocks a vendor and the application must tolerate it.

Do not block identity, feature-flag, fraud, payment, or telemetry endpoints merely because they are on another host. Their absence may change core application behavior or erase valuable failure evidence. Classify dependencies with product owners and security teams.

4. Parameterized Playwright Route Abort Examples for Error Codes

A small parameterized suite can verify that the same fallback handles several network categories. Use the exact supported code strings and keep the product assertion stable. Avoid testing every code if the application intentionally treats them as one generic network error. More cases are justified only when recovery differs.

import { test, expect } from '@playwright/test';

const failureCases = [
  { code: 'connectionrefused', label: 'connection refused' },
  { code: 'internetdisconnected', label: 'offline' },
  { code: 'namenotresolved', label: 'dns failure' },
  { code: 'timedout', label: 'timeout' },
] as const;

for (const failureCase of failureCases) {
  test(`reports ${failureCase.label}`, async ({ page }) => {
    await page.goto('https://example.com');
    await page.route('**/api/status', route =>
      route.abort(failureCase.code),
    );

    const failed = page.waitForEvent(
      'requestfailed',
      request => request.url().endsWith('/api/status'),
    );

    await page.evaluate(() => fetch('/api/status').catch(() => undefined));

    const request = await failed;
    expect(request.failure()).not.toBeNull();
  });
}

The labels improve report readability. The test does not assert exact errorText because the rendering of network failure details can vary by browser. For an application suite, replace the final assertion with the same user-facing fallback plus any differentiated action, such as an offline queue only for disconnected mode.

timedout immediately causes a timeout-class failure. It does not wait for the application's actual client timeout or validate how a delayed server consumes resources. When real timing is the requirement, use a controlled slow endpoint and the client's configured timeout in a separate integration test. Route abort is ideal for deterministic branch coverage.

5. Abort Only the First Attempt to Test Retry Recovery

Permanent failure tests prove the error state. A one-shot failure proves recovery. Count matching attempts, abort the first, then fulfill or continue the second. This makes the fault window explicit and avoids removing the route between closely timed requests.

import { test, expect } from '@playwright/test';

test('second request succeeds after first request resets', async ({ page }) => {
  let attempts = 0;

  await page.goto('https://example.com');
  await page.route('**/api/cart', async route => {
    attempts += 1;

    if (attempts === 1) {
      await route.abort('connectionreset');
      return;
    }

    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      json: { items: [{ sku: 'QA-101', quantity: 1 }] },
    });
  });

  const firstResult = await page.evaluate(async () => {
    try {
      await fetch('/api/cart');
      return 'success';
    } catch {
      return 'failed';
    }
  });

  const secondResult = await page.evaluate(async () => {
    const response = await fetch('/api/cart');
    return response.json();
  });

  expect(firstResult).toBe('failed');
  expect(secondResult).toEqual({ items: [{ sku: 'QA-101', quantity: 1 }] });
  expect(attempts).toBe(2);
});

In a UI test, the second request should come from the product's retry button or automatic retry policy, not a direct fetch. Assert that the button becomes enabled, duplicate mutations do not occur, input remains intact, and the success state appears exactly once.

Be careful with automatic retries in application clients. One click may create several attempts before the assertion reaches the page. Count and record attempts, then configure the injected fault around the product's actual policy. For POST requests, verify idempotency and duplicate prevention as part of API error handling and negative testing.

6. Compose Abort Rules With Fallback Instead of Continue

Teams often have a shared route policy plus test-local exceptions. fallback hands the request to the next matching handler. Matching handlers run in reverse registration order, so the last registered rule evaluates first. continue bypasses the rest of the chain and sends the request to the network immediately.

import { test, expect } from '@playwright/test';

test('layers mutation protection over normal traffic', async ({ page }) => {
  const handled: string[] = [];

  await page.route('**/*', async route => {
    handled.push(`base:${route.request().method()}`);
    await route.continue();
  });

  await page.route('**/api/**', async route => {
    const method = route.request().method();

    if (method === 'DELETE') {
      handled.push('blocked:DELETE');
      await route.abort('accessdenied');
      return;
    }

    await route.fallback();
  });

  await page.goto('https://example.com');
  await page.evaluate(() =>
    fetch('/api/item/42', { method: 'DELETE' }).catch(() => undefined),
  );

  expect(handled).toContain('blocked:DELETE');
  expect(handled).not.toContain('base:DELETE');
});

The API handler was registered last and receives DELETE first. It aborts, so the base handler never sees that request. For a GET it would call fallback, then the base handler would continue it.

Use chains sparingly. A single local handler with explicit branches is easier to read than many global handlers. Layering makes sense when fixtures own independent policies, such as credential headers, mutation protection, and test-local failures. Document registration order in the fixture and test it with a small contract suite. The distinction between forwarding and delegation is also central to Playwright route continue with override.

7. Cover Popups and New Tabs With Context Routing

A page route applies to one page. A context route covers every page in that browser context, including a popup's first navigation when it is registered before the opening action. This is the correct scope for payment windows, federated login popups, and links that open another tab when the same injected policy should apply.

import { test, expect } from '@playwright/test';

test('context policy applies to two pages', async ({ context, page }) => {
  const failures: string[] = [];

  await context.route('**/api/optional-widget', async route => {
    failures.push(route.request().url());
    await route.abort('blockedbyclient');
  });

  await page.goto('https://example.com');
  await page.evaluate(() =>
    fetch('/api/optional-widget').catch(() => undefined),
  );

  const secondPage = await context.newPage();
  await secondPage.goto('https://example.com');
  await secondPage.evaluate(() =>
    fetch('/api/optional-widget').catch(() => undefined),
  );

  expect(failures).toHaveLength(2);
});

In an actual popup test, create const popupPromise = page.waitForEvent('popup') before clicking, then await the popup. Do not wait for the popup before the action that creates it. The context route should already be active.

Context-wide handlers have a larger blast radius. Use an exact endpoint or host, and remove a temporary rule if later pages should use real networking. A fresh Playwright Test context per test prevents most cross-test leakage, but custom shared-context fixtures require disciplined cleanup.

8. Package a Reusable Abort Helper With Cleanup

Repeated inline handlers invite slightly different matchers and forgotten cleanup. A small helper can install one documented rule, capture evidence, and return a cleanup function. Keep the helper transparent so tests still state the injected failure.

// support/network-fault.ts
import type { Page, Route } from '@playwright/test';

export async function abortEndpoint(
  page: Page,
  urlPattern: string,
  errorCode = 'failed',
) {
  const requests: string[] = [];

  const handler = async (route: Route) => {
    requests.push(route.request().url());
    await route.abort(errorCode);
  };

  await page.route(urlPattern, handler);

  return {
    requests,
    cleanup: () => page.unroute(urlPattern, handler),
  };
}
import { test, expect } from '@playwright/test';
import { abortEndpoint } from '../support/network-fault';

test('scopes a profile fault', async ({ page }) => {
  await page.goto('https://example.com');
  const fault = await abortEndpoint(
    page,
    '**/api/profile',
    'connectionrefused',
  );

  await page.evaluate(() => fetch('/api/profile').catch(() => undefined));
  expect(fault.requests).toHaveLength(1);

  await fault.cleanup();
});

For stricter TypeScript, define an application-owned union of the documented codes rather than accept any string. Do not invent a wrapper method such as route.fail, because Playwright has no such API. The helper should ultimately call the supported route methods.

A fixture can perform cleanup automatically, but it should still expose the rule and captured requests to the test. Hidden global fault injection produces confusing failures when engineers do not know the network was changed.

9. Add Observability Without Brittle Error Assertions

Collect only evidence that helps distinguish an injected fault from an unexpected outage. Useful fields include URL, method, resource type, selected abort code, attempt number, and test step. Avoid logging authorization headers, cookies, post bodies, or personal data.

import { test, expect } from '@playwright/test';

test('captures concise failure evidence', async ({ page }, testInfo) => {
  const evidence: Array<Record<string, string>> = [];

  await page.route('**/api/search**', async route => {
    const request = route.request();
    evidence.push({
      method: request.method(),
      resourceType: request.resourceType(),
      url: request.url(),
      injectedCode: 'timedout',
    });
    await route.abort('timedout');
  });

  await page.goto('https://example.com');
  await page.evaluate(() => fetch('/api/search?q=playwright').catch(() => undefined));

  expect(evidence).toHaveLength(1);
  await testInfo.attach('network-fault.json', {
    body: Buffer.from(JSON.stringify(evidence, null, 2)),
    contentType: 'application/json',
  });
});

Attachments travel with Playwright reports and are easier to inspect than thousands of console lines. Sanitize URLs if query strings may contain secrets or customer data. In many suites, attaching only on failure is a better retention policy.

Pair infrastructure evidence with application assertions. Check an accessible alert, disabled or enabled retry state, preserved inputs, absence of duplicate transactions, and successful recovery. A passed expect(evidence).toHaveLength(1) proves interception, not product quality.

10. Apply Playwright Network Failure Best Practices

Choose the smallest fault that exercises the requirement. Endpoint aborts are precise. Host aborts test optional dependency loss. Context offline mode tests broad disconnection. A 503 fulfillment tests error-response handling. A delayed test service validates real client timeouts. These controls are related, but they are not interchangeable.

Make timing deterministic: register the handler, create event promises, trigger the action, then await outcomes. Avoid waitForTimeout. Keep one terminal action per route and return after conditional aborts. Use fallback only when handler composition is intentional.

Treat the injected network rule as part of the scenario's Given state. Name the test accordingly, record the chosen code, and remove the rule when the state ends. Keep credentials and request bodies out of logs. Prefer locally controlled targets in CI so a public website or third-party service does not determine whether the test can start.

Review coverage for false confidence. A simulated timeout proves the application's timeout branch under a browser-labelled failure. It does not prove a load balancer, proxy, mobile network, or delayed backend behaves identically. Combine fast deterministic fault injection with fewer environment-level resilience tests where the full infrastructure matters.

Interview Questions and Answers

Q: Show a simple route abort example.

Register await page.route('**/api/orders', route => route.abort('connectionrefused')) before the action that calls orders. Start a matching requestfailed promise, perform the action, and await the failure. Then assert the application's error and retry states.

Q: How would you test that retry succeeds after one network failure?

Count matching requests in the route handler. Abort attempt one and continue or fulfill attempt two. Trigger retry through the UI and assert one failure, one recovery, preserved state, and no duplicate mutation.

Q: Why use fallback in a shared route design?

fallback delegates to the next matching handler, allowing independent policies to compose. continue sends the request immediately and skips the remaining handlers. I use fallback only when registration order is documented and covered by a small infrastructure test.

Q: How would you block only a known analytics vendor?

I use a URL predicate and compare url.hostname against an allowlisted set of optional vendor hosts. Exact hostname checks avoid substring mistakes. I also verify that product APIs remain untouched and that blocking matches the test purpose.

Q: What should a reusable network-fault helper return?

It should expose captured requests or attempt counts plus an explicit cleanup function. The test should still name the endpoint and failure code so the injected behavior is visible. A wrapper should call supported Playwright route APIs rather than conceal invented behavior.

Q: How do you test popups with the same abort policy?

I register a browser-context route before the action that opens the popup. Context scope covers current pages and new pages, including early popup requests. The matcher remains narrow because the policy affects the whole context.

Q: What evidence belongs in a route-abort report?

I record sanitized URL, method, resource type, injected code, and attempt number. I attach it to the test report when useful, while excluding cookies, authorization headers, request bodies, and sensitive query values.

Q: Is timedout equivalent to waiting for a real timeout?

No. It deterministically produces a timeout-class request failure. It is useful for branch coverage, but elapsed-time, proxy, and resource behavior require a controlled delayed endpoint or environment-level test.

Common Mistakes

  • Copying a broad **/* example into a shared fixture without a pass-through branch.
  • Treating route.abort('timedout') as proof of real end-to-end timeout duration.
  • Blocking third-party hosts by substring and accidentally matching an unrelated or hostile hostname.
  • Asserting infrastructure counters while omitting the user-visible failure and recovery outcomes.
  • Retrying a mutating POST without verifying idempotency or duplicate prevention.
  • Using page scope for a policy that must cover a popup's initial request.
  • Calling continue when the intention is to let another matching route handler inspect the request.
  • Logging headers, bodies, tokens, or sensitive query strings as network evidence.
  • Leaving a reusable handler installed after the scenario's fault window.
  • Assuming emulated transport failures replace a smaller set of full-environment resilience tests.

Conclusion

Effective Playwright route abort examples are precise fault experiments. They name the dependency, install the failure before the trigger, observe the matching failed request, and assert what the user can do next. One-shot handlers cover recovery, context routes cover multi-page flows, and fallback supports carefully designed policy layers.

Choose one example that matches your current risk and adapt its application assertions first. Once the behavior is stable, extract a transparent helper, capture sanitized evidence, and add only the failure variants that lead to distinct product decisions.

Interview Questions and Answers

How would you design a route-abort retry test?

I count attempts in a precise route handler, abort the first, and allow or fulfill the second. The UI triggers both attempts, not test-side fetch calls. I assert the initial error, preserved state, successful retry, and absence of duplicate server mutations.

What is a safe pattern for blocking third-party traffic?

I match parsed hostnames against an explicit reviewed set and abort with `blockedbyclient`. I avoid broad substrings and confirm the hosts are optional for the scenario. The test still asserts the core user outcome and may record which vendor requests were blocked.

How would you compose a global route with a test-specific abort?

I register the global handler first and the test-specific handler later. The specific handler aborts its target and calls `fallback` for everything else, allowing the earlier handler to run. I do not call `continue` when delegation is required.

Why attach network-fault evidence to a report?

It proves which request, method, and injected code the scenario controlled, helping separate an expected fault from an unrelated outage. I sanitize URLs and never include tokens, cookies, authorization headers, or sensitive request bodies.

When is resource-type blocking inappropriate?

It is inappropriate when blocked images, fonts, media, styles, or scripts affect the behavior under test. Layout, lazy loading, callbacks, accessibility, and screenshots can all change. I use it only for a documented scenario whose assertions remain valid.

How do you avoid sleeps in network-failure tests?

I create a predicate-based event promise before the triggering action, then await the event and the UI assertion. Attempt counters and exact matchers provide deterministic synchronization. `waitForTimeout` is unnecessary and creates timing sensitivity.

What is the difference between context offline mode and route abort?

Offline mode affects network connectivity for the whole browser context. Route abort targets selected matching requests and can use a chosen failure category. I use route abort for dependency-level faults and offline mode for broad disconnected behavior.

What belongs in a reusable abort helper?

The helper should accept an explicit page or context, matcher, and documented error code. It should expose observed requests or attempts and provide cleanup. The abstraction must keep the injected fault visible to readers rather than becoming hidden global behavior.

Frequently Asked Questions

What is a basic Playwright route abort example?

Register `page.route('**/api/items', route => route.abort())` before the request is sent. Await a matching `requestfailed` event and assert the application's error state.

How do I abort only the first request in Playwright?

Increment an attempt counter in the route handler. Abort when the counter is one, then continue or fulfill subsequent attempts so the test can verify recovery.

Can I abort requests from a Playwright popup?

Yes. Register `browserContext.route()` before the action that creates the popup. The context-level policy applies to pages in that context, including new tabs and popups.

How do I block third-party requests safely?

Use a URL predicate with exact hostname comparison against a reviewed set. Keep the matcher narrow, document why each dependency is optional, and verify core product requests still run.

Should I use abort or fulfill for a 500 error test?

Use `route.fulfill({ status: 500 })` for an HTTP 500 response. Use `route.abort()` when no response should exist because the transport, DNS, connection, or client blocking failed.

How do I clean up a Playwright route handler?

Keep a reference to the handler and call `page.unroute(pattern, handler)` or the context equivalent. Cleanup is especially important when the same page or custom context remains active after the fault scenario.

Can Playwright route abort simulate offline mode?

The `internetdisconnected` code can fail selected routes with an offline-style category. To make the entire context offline, `browserContext.setOffline(true)` is the broader control.

Related Guides