Resource library

QA How-To

How to Use Playwright route continue with override (2026)

Learn Playwright route continue with override for headers, methods, post data, and URLs, including handler order, redirects, security, and TypeScript tests.

21 min read | 2,867 words

TL;DR

Call `route.continue({ headers, method, postData, url })` inside a page or browser-context route to send an overridden request to the network. Preserve required headers, keep rewritten URLs on the original protocol, use `fallback` for handler chaining, and verify the server received the intended request.

Key Takeaways

  • Use `route.continue()` to send the matched request immediately with optional URL, method, headers, or post data changes.
  • Copy existing headers before adding or removing values so required application headers are preserved.
  • Change cookies with browser-context cookie APIs because forbidden request headers may ignore overrides.
  • Keep URL rewrites on the same protocol and remember that matching used the original URL.
  • Use `fallback()` instead when another matching route handler must run.
  • Verify overrides at a controlled echo endpoint or test server, then assert the user-visible result.
  • Scope mutation narrowly and prevent secrets from reaching logs, traces, or unintended hosts.

Playwright route continue with override intercepts a browser request, changes supported request fields, and sends it to the network. The override object can provide headers, method, postData, and url, which makes the API useful for test-only headers, controlled backend rewrites, request-shape experiments, and temporary compatibility checks.

Request mutation is powerful because it changes what the server receives without changing application code. It also creates risk: a broad handler can alter navigation, leak credentials, bypass other route handlers, or produce a test environment that no user experiences. This guide shows precise TypeScript patterns and the limits that matter in 2026.

TL;DR

await page.route('**/api/orders', async route => {
  const request = route.request();
  const headers = {
    ...request.headers(),
    'x-qa-run-id': 'checkout-smoke-101',
  };

  await route.continue({ headers });
});
Override Accepted value Important limit
headers Header map Applies to the request and its redirects, forbidden headers may be ignored
method HTTP method string Applies only to the original request, not redirects
postData String, buffer, or serializable value Keep body and content type consistent
url Same-protocol URL string Route matching still uses the original URL

Use continue when the request should go to the network now. Use fallback when another matching handler should inspect it first. Use fulfill when the test should provide a response instead of sending the request.

1. What Playwright Route Continue With Override Does

A route handler receives the matched Route and its associated Request. Calling route.continue() sends that request to the network. Supplying an options object replaces supported fields for that outbound operation. The browser then receives the real server response unless some network layer outside Playwright changes it.

The operation is not a response mock. If the target returns 401, 404, or 500, the test receives that real response. To synthesize a response, use route.fulfill. To obtain and patch a real response, use route.fetch followed by route.fulfill. To fail the transport, use route.abort. See how to use Playwright route abort for failure-focused scenarios.

Overrides are scoped to the matched request. They do not rewrite application JavaScript, service-worker logic, or every future request unless the route pattern continues to match. Header overrides are carried into redirects initiated by the request. URL, method, and post data overrides apply only to the original request and are not automatically carried to redirected requests.

route.continue() is terminal for Playwright routing. It immediately sends the request and does not call other matching handlers. This is often correct for one local handler. In layered fixtures, it can unintentionally skip a previously registered policy. Use route.fallback() with the same types of overrides when the next matching handler must run.

2. Set Up a Runnable Request Override Test

A request-echo endpoint is the cleanest way to prove what the server received. The following test navigates to httpbin's /anything endpoint, adds a test header, parses the JSON response, and finds the header without assuming how the server capitalizes its name. It requires normal outbound network access.

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

test('sends an added QA header to the server', async ({ page }) => {
  await page.route('https://httpbin.org/anything', async route => {
    const headers = {
      ...route.request().headers(),
      'x-qa-run-id': 'run-101',
    };

    await route.continue({ headers });
  });

  const response = await page.goto('https://httpbin.org/anything');
  expect(response?.ok()).toBeTruthy();

  const payload = await response!.json() as { headers: Record<string, string> };
  const qaHeader = Object.entries(payload.headers).find(
    ([name]) => name.toLowerCase() === 'x-qa-run-id',
  );

  expect(qaHeader?.[1]).toBe('run-101');
});

For CI, prefer an echo endpoint inside a controlled test service. A public service can be unavailable, rate-limited, or blocked by corporate networking. The test structure stays the same: register, trigger, inspect the response, and assert the received values.

Install the route before the triggering navigation or click. If a page issues the request during startup, a handler registered after page.goto is too late. Use the smallest URL matcher that represents the desired request. Exact URLs are excellent for a single echo test, while a full host and path predicate works well when query parameters vary.

3. Add, Replace, and Remove Request Headers

Start from request.headers() so authentication, accepted content types, browser metadata, and application correlation fields remain present. Object assignment replaces an existing header case-insensitively at the protocol level. Deleting a lowercased key from the copied map removes a header Playwright is allowed to control.

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

test('changes test headers and removes a legacy header', async ({ page }) => {
  await page.route('**/api/**', async route => {
    const headers = { ...route.request().headers() };

    headers['x-feature-variant'] = 'candidate';
    headers['x-qa-suite'] = 'checkout';
    delete headers['x-legacy-client'];

    await route.continue({ headers });
  });

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

This test demonstrates valid routing syntax but does not claim the public page sends an /api request. In an application test, trigger the endpoint and assert its server-observed headers or the feature behavior they select. Do not assert only that the handler ran because that does not prove an upstream proxy preserved the override.

Some headers are forbidden or managed by the browser, including values such as Cookie, Host, and Content-Length. An attempted override may be ignored and the original value used. Set cookies with browserContext.addCookies() or authenticated storage state. Let the browser calculate body length. Do not use a Host override to simulate DNS or tenancy. Route to a controlled same-protocol URL or configure the test environment properly.

If every request needs a stable header, top-level use.extraHTTPHeaders or a project-level value is clearer than a catch-all route. Use continue-time mutation when the header depends on a specific URL, method, test phase, or request body.

4. Remove Sensitive or Conditional Headers Safely

Header removal is valuable for negative authentication tests, backward-compatibility checks, and proving that an optional client header is not required. Narrow the matcher so page documents and unrelated APIs retain their normal credentials. Never forward an authorization value from one host to another during a URL rewrite.

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

test('removes authorization from one public endpoint', async ({ page }) => {
  await page.route('**/api/public-profile', async route => {
    const headers = { ...route.request().headers() };
    delete headers.authorization;

    await route.continue({ headers });
  });

  await page.goto('https://example.com');

  const result = await page.evaluate(async () => {
    const response = await fetch('/api/public-profile', {
      headers: { authorization: 'Bearer test-token' },
    });
    return response.status;
  });

  expect(typeof result).toBe('number');
});

The endpoint on example.com returns its real status, so the assertion stays generic. In a controlled application, assert the documented public response and verify server logs in a safe test environment if necessary. Never place a real bearer token in source code. Use a clearly fake token for mechanics or inject a secret through protected runtime configuration when an integration test truly needs it.

Header names returned by Playwright are generally normalized, so lowercased lookup and deletion are reliable. When adding fields, lowercased names reduce duplicate-looking entries in JavaScript objects. HTTP semantics are case-insensitive even if an echo service presents a display form.

Redact headers in custom logs and report attachments. Traces and server diagnostics may already capture request metadata according to your tool configuration, so define retention and access controls. Request mutation tests should strengthen security coverage, not create a new secret-distribution path.

5. Override Method and Post Data Together

Changing a method without changing its body and content type can create an invalid request. Treat method, post data, and relevant headers as one request contract. A common controlled example converts a GET navigation to a POST accepted by an echo endpoint.

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

test('converts a request to JSON POST', async ({ page }) => {
  await page.route('https://httpbin.org/anything', async route => {
    const headers = {
      ...route.request().headers(),
      'content-type': 'application/json',
    };

    await route.continue({
      method: 'POST',
      headers,
      postData: JSON.stringify({ source: 'playwright', approved: true }),
    });
  });

  const response = await page.goto('https://httpbin.org/anything');
  expect(response?.ok()).toBeTruthy();

  const payload = await response!.json() as {
    method: string;
    json: { source: string; approved: boolean };
  };

  expect(payload.method).toBe('POST');
  expect(payload.json).toEqual({ source: 'playwright', approved: true });
});

This pattern is useful for protocol experiments and test-environment adapters, but it should not become a substitute for testing what the application actually sends. If production JavaScript should make a POST, the best functional test normally lets it do so and asserts the result. A route override can temporarily unblock dependent testing or validate server tolerance while a client change is under development.

For an existing JSON body, parse defensively. request.postDataJSON() is convenient when the payload is JSON or form-encoded, but it can throw or misrepresent arbitrary binary data. request.postData() returns a string or null, while request.postDataBuffer() supports bytes. Choose the method that matches the content type and avoid logging sensitive payloads.

Changing a request to GET while retaining a body is not portable behavior. Remove or omit postData and adjust headers according to the endpoint contract.

6. Rewrite a Request URL Within the Same Protocol

The url override can send a matched request to a different target, but the new URL must use the same protocol as the original. An HTTPS request can be rewritten to another HTTPS URL, not to HTTP. This restriction prevents a route override from silently crossing the security boundary between secure and insecure transport.

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

test('rewrites a catalog request to an HTTPS fixture endpoint', async ({ page }) => {
  await page.route('https://httpbin.org/anything/original', async route => {
    await route.continue({
      url: 'https://httpbin.org/json',
    });
  });

  await page.goto('https://httpbin.org/');
  const result = await page.evaluate(async () => {
    const response = await fetch('/anything/original');
    return { status: response.status, body: await response.json() };
  });

  expect(result.status).toBe(200);
  expect(result.body).toBeTruthy();
});

For a cross-host rewrite, use a fresh unauthenticated context and do not assume that deleting the Cookie header through an override will work. Prefer a controlled fixture host, validate the destination against an allowlist, and never build the target directly from untrusted page data. Cross-origin response access can also be governed by browser CORS rules, depending on request mode and the destination response. The illustrative endpoints support common testing, but an internal fixture server gives stronger control.

Route matching is based on the original URL. Rewriting to /json does not cause handlers registered for the new URL to run again. This prevents recursive rematching but surprises engineers expecting a second route pass. If policy layers need to examine an overridden request, use fallback and understand that subsequent matching still refers to the original route URL.

URL overrides apply only to the original request. If the server redirects, the override URL is not reapplied to each redirect. Header overrides, in contrast, apply to redirects initiated by that request, which is another reason to limit sensitive custom headers.

7. Compare Continue, Fallback, Fulfill, Fetch, and Abort

Choosing the wrong route method produces a scenario that looks plausible but tests different behavior. Use this decision table during reviews.

Method Sends network traffic Can override request Invokes next matching handler Provides or changes response
continue Yes Yes No No
fallback Eventually Yes Yes No
fulfill No, unless using a fetched response Not its purpose No Yes
fetch Yes Yes No by itself Returns response for later fulfill
abort No completed request Error code only No No response

Use continue for a terminal pass-through with optional mutation. Use fallback for middleware-style composition. Use fulfill for deterministic synthetic responses. Use fetch plus fulfill to patch a real response body, headers, or status. Use abort for a transport failure. Playwright route abort examples provides focused fault patterns.

await page.route('**/*', route => route.continue());

await page.route('**/api/**', async route => {
  const headers = {
    ...route.request().headers(),
    'x-api-test': 'true',
  };

  await route.fallback({ headers });
});

Because matching handlers run in reverse registration order, the API handler runs first. It adds a header and falls back to the earlier catch-all, which continues. Replacing fallback with continue would skip the catch-all. In this tiny example the outcome seems similar, but a real base handler may add credentials, log evidence, or enforce safety.

Keep layered routing short and documented. If many fixtures register **/*, handler order becomes a hidden framework. Prefer endpoint-specific matchers and a single owner for global policy.

8. Verify Overrides at the Server Boundary

Inspecting the override object in test code proves only what you intended to pass. A proxy, browser restriction, redirect, gateway, or server normalization can change what arrives. Verify at a controlled echo endpoint, test server log, or response that reflects the received request. Then assert the product behavior produced by that request.

For headers, compare names case-insensitively. For JSON bodies, compare parsed objects rather than raw property order. For URLs, assert the destination response or server record. For methods, verify the server-observed method. Do not use page.waitForTimeout to guess that traffic finished. Await a response or application state after the triggering action.

const responsePromise = page.waitForResponse(
  response => response.url().includes('/api/orders') && response.status() === 200,
);

await page.getByRole('button', { name: 'Load orders' }).click();
const response = await responsePromise;
expect(response.request().method()).toBe('POST');
await expect(page.getByRole('heading', { name: 'Orders' })).toBeVisible();

Depending on the browser event surface and rewrite, request URLs in Playwright diagnostics can emphasize original or routed identity. Treat the controlled server's observation as authoritative for the outbound target and payload. Use the trace to correlate the route, response, and UI, but avoid exact assertions on internal display details that are not part of the product contract.

Negative testing still matters. Verify what happens when the overridden credential is rejected, the alternate backend returns a schema change, or the method is unsupported. API error handling and negative testing helps turn request mutation into meaningful contract coverage.

9. Build a Reusable, Safe Override Helper

A helper can standardize allowlisted hosts, sanitized evidence, and cleanup. It should not accept arbitrary destinations from the page. The example below adds headers to one explicit pattern and returns a cleanup function.

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

export async function addHeadersForRoute(
  page: Page,
  pattern: string,
  addedHeaders: Record<string, string>,
) {
  const handler = async (route: Route) => {
    await route.continue({
      headers: {
        ...route.request().headers(),
        ...addedHeaders,
      },
    });
  };

  await page.route(pattern, handler);
  return () => page.unroute(pattern, handler);
}
const cleanup = await addHeadersForRoute(page, '**/api/orders', {
  'x-test-scenario': 'empty-cart',
});

await page.getByRole('button', { name: 'Load orders' }).click();
await cleanup();

Restrict callers through higher-level helpers when a header selects powerful test behavior. For example, a test-only impersonation header should be accepted only in an isolated nonproduction environment, generated from protected credentials, and rejected by production infrastructure. Client-side routing is not an authorization control.

Prefer Playwright config extraHTTPHeaders when a stable header belongs to every request in a project. Prefer API request-context headers when testing APIs without a browser. Use route overrides only when browser traffic needs conditional, request-level mutation. Clear ownership reduces invisible interactions among fixtures.

10. Debug Playwright Route Continue With Override

If the server does not receive an override, first prove the handler matched. Log a sanitized URL and method temporarily, confirm registration precedes the request, and verify the full URL pattern. Check whether a service worker handles the request before native routing. When Playwright's page or context routes must own interception, configure serviceWorkers: 'block'; do not block them when worker behavior is under test.

If one handler never runs, inspect handler order. A later matching handler may call continue, abort, or fulfill, all of which terminate the chain. Change it to fallback only if delegation is the intended design. Page routes also take precedence over context routes when both match.

If a header does not change, it may be forbidden or controlled by the browser. Use context cookie APIs for cookies, keep the original Host, and let the browser manage Content-Length. Verify the server observation rather than trusting a local object. If a body is corrupt, align method, content type, encoding, and post data.

If a URL rewrite fails, confirm the protocol is unchanged, the destination is reachable, credentials were removed before a host change, and CORS permits the browser's use of the response. Remember that matching uses the original URL.

Finally, ensure the override is not hiding a product defect. A permanent route adapter can make tests pass while production clients send the wrong contract. Give temporary adapters an owner and removal condition, and keep at least one unmodified contract test against the actual client request.

Interview Questions and Answers

Q: What fields can route.continue override?

It can override URL, method, headers, and post data. The rewritten URL must use the same protocol as the original. Headers apply to redirects, while URL, method, and post data affect only the original request.

Q: What is the difference between continue and fallback?

Continue sends the request to the network immediately and skips other matching handlers. Fallback delegates to the next matching handler and can carry overrides through that route chain. I use fallback for intentional middleware composition.

Q: How would you add one header without losing existing headers?

I spread route.request().headers() into a new object and add the desired lowercase key afterward. Then I call route.continue({ headers }). I verify the value at a controlled server boundary.

Q: Can you override the Cookie or Host header?

Browsers restrict several headers, and an attempted override may be ignored. I set cookies through browserContext.addCookies() or storage state and do not spoof Host. For alternate backends, I use an allowlisted same-protocol URL rewrite and remove credentials before crossing hosts.

Q: How do you modify a JSON body safely?

I confirm the content type, parse the original body only if it is valid for that endpoint, create the new object, serialize it, and set a matching content-type header. I avoid logging payloads that may contain credentials or personal data.

Q: Why might a context route be skipped?

A page-level route can take precedence, or a later matching handler can terminate routing with continue, fulfill, or abort. A service worker can also handle traffic before native routing. I inspect route registration, scope, order, and the actual request path.

Q: How do you prove an override reached the server?

I use a controlled echo endpoint, test-server observation, or response that reflects the received method, headers, URL, and body. I assert parsed values and then the visible application result. Inspecting only the local options object is not enough.

Q: When is route continue with override the wrong tool?

It is wrong when the test needs a synthetic response, response modification, transport failure, or a stable header on every project request. Fulfill, fetch plus fulfill, abort, or extraHTTPHeaders communicates those intentions more directly.

Common Mistakes

  • Replacing the entire header map and accidentally dropping authentication or required content-negotiation headers.
  • Trying to override forbidden headers such as Cookie, Host, or Content-Length and assuming the browser accepted it.
  • Changing a method without aligning post data and content type.
  • Rewriting HTTPS to HTTP even though the override URL must keep the original protocol.
  • Forwarding authorization or cookies to a different host during a URL rewrite.
  • Calling continue in a layered design where another matching handler must still run.
  • Expecting the rewritten URL to be matched again by handlers registered for that new URL.
  • Verifying only local callback execution instead of what the controlled server received.
  • Keeping a temporary compatibility rewrite indefinitely and masking a wrong production client contract.
  • Logging sensitive headers or request bodies while debugging.

Conclusion

Playwright route continue with override is the right tool when a matched browser request should still reach a real server after a controlled change to its headers, method, post data, or same-protocol URL. Preserve the original request contract where possible, mutate only the intended field, and remember that continue ends Playwright's handler chain.

Start with a narrow echo-endpoint test, verify the server observation, then apply the pattern to one application behavior. Use fallback for composition, cookie APIs for cookies, and explicit cleanup for temporary handlers. The result is a transparent test control rather than a hidden adapter that makes the suite pass for the wrong reason.

Interview Questions and Answers

Explain `route.continue()` with overrides.

It sends a matched request to the network with optional changes to headers, method, post data, or same-protocol URL. It is terminal for Playwright's route chain, so other matching handlers do not run. I use it for precise request mutation and verify the result at the server boundary.

How would you safely override request headers?

I copy the original header map, change only reviewed lowercase keys, and preserve required authentication and content negotiation. I never log secrets and I verify the server received the value. For stable project-wide headers, I prefer `extraHTTPHeaders`.

When would you choose fallback over continue?

I choose fallback when another matching handler must inspect or enforce policy on the request. Continue bypasses that chain and sends immediately. In layered fixtures, registration order and terminal behavior must be explicit.

What restrictions apply to URL overrides?

The new URL must use the same protocol as the original. Matching still uses the original URL, and the URL override does not carry across redirects. Before changing hosts, I remove credentials and validate the destination against a controlled allowlist.

How do you modify a request body without corrupting it?

I first identify the payload type and content type. For JSON, I parse only valid JSON, update the object, serialize it, and keep the content-type header aligned. For binary data I use the buffer APIs and avoid lossy string conversion.

Why is server-side verification important for request overrides?

The callback shows the intended override, but browsers, proxies, redirects, and gateways can normalize or reject fields. A controlled echo endpoint or test server proves what arrived. I then assert the application behavior, which is the actual QA outcome.

How do service workers affect request overrides?

A service worker can handle requests before native page or context routing observes them. If the test contract is native Playwright routing, I can block service workers in the context. If worker behavior matters, I keep them enabled and test that path directly.

What security risks do request overrides create?

A broad rule can leak authorization, cookies, bodies, or test-only privileges to the wrong destination. I use narrow matchers, allowlisted hosts, protected runtime secrets, sanitized reporting, and nonproduction enforcement. Client-side mutation is never treated as an authorization boundary.

Frequently Asked Questions

How do I override headers with route.continue in Playwright?

Copy `route.request().headers()`, add or remove the intended keys, and call `await route.continue({ headers })`. Verify the received values at a controlled server endpoint.

Can Playwright route.continue change the request URL?

Yes, pass a `url` value in the override object. The new URL must have the same protocol as the original, and route matching remains based on the original URL.

What is the difference between route.continue and route.fallback?

`route.continue()` sends the request immediately and skips other matching handlers. `route.fallback()` delegates to the next matching handler and is appropriate for layered route policies.

Can route.continue override a POST body?

Yes. Pass `postData` and keep the method, content type, and body encoding consistent. For JSON, serialize the object and set `content-type: application/json` when necessary.

Why did my Cookie header override not work?

Cookie is among the headers browsers may forbid or manage, so an override can be ignored. Use `browserContext.addCookies()` or an appropriate storage-state file instead.

Do route.continue header overrides apply to redirects?

Yes, header overrides apply to the routed request and redirects it initiates. URL, method, and post data overrides apply only to the original request.

How should I test that a request override worked?

Use a controlled echo server or application test endpoint to inspect the received method, headers, URL, and body. Then assert the user-visible result created by that server interaction.

Related Guides