microsoft/playwright · error · Error

Cannot fulfill with redirect status: ${response.status}

Error message

Cannot fulfill with redirect status: ${response.status}

What it means

route.fulfill() rejects HTTP 3xx statuses because Playwright fulfills a request by injecting a synthetic response, and a redirect response cannot be synthesized client-side without a Location header negotiation the network layer does not perform. fulfill() throws on any status in the 300-399 range.

Source

Thrown at packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts:127

  private readonly _session: WKSession;
  private readonly _requestId: string;

  constructor(session: WKSession, requestId: string) {
    this._session = session;
    this._requestId = requestId;
  }

  async abort(errorCode: string) {
    const errorType = errorReasons[errorCode];
    assert(errorType, 'Unknown error code: ' + errorCode);
    // In certain cases, protocol will return error if the request was already canceled
    // or the page was closed. We should tolerate these errors.
    await this._session.sendMayFail('Network.interceptRequestWithError', { requestId: this._requestId, errorType });
  }

  async fulfill(response: types.NormalizedFulfillResponse) {
    if (300 <= response.status && response.status < 400)
      throw new Error('Cannot fulfill with redirect status: ' + response.status);

    // In certain cases, protocol will return error if the request was already canceled
    // or the page was closed. We should tolerate these errors.
    let mimeType = response.isBase64 ? 'application/octet-stream' : 'text/plain';
    const headers = headersArrayToObject(response.headers, true /* lowerCase */);
    const contentType = headers['content-type'];
    if (contentType)
      mimeType = contentType.split(';')[0].trim();

    await this._session.sendMayFail('Network.interceptRequestWithResponse', {
      requestId: this._requestId,
      status: response.status,
      statusText: network.statusText(response.status),
      mimeType,
      headers,
      base64Encoded: response.isBase64,
      content: response.body
    });

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. To mock a redirect, fulfill with the FINAL (2xx) response after following the redirect yourself, or use route.continue() with overridden url to redirect at the request layer.
  2. Validate status<300 || status>=400 before calling fulfill; reject 3xx in your route logic.
  3. If you only need the redirect chain's terminal body, capture and fulfill that body with status 200.

Example fix

// before
await route.fulfill({ status: 302, headers: { location: '/x' } });

// after
await route.continue({ url: 'https://site/x' });
Defensive patterns

Strategy: validation

Validate before calling

function isFulfillableStatus(s) { return s < 300 || s >= 400; }
if (isFulfillableStatus(response.status)) await route.fulfill(response);
else await route.continue({ url: response.headers.location });

Type guard

function isRedirectStatus(s: number): boolean { return s >= 300 && s < 400; }

Try / catch

try { await route.fulfill(resp); }
catch (e) {
  if (/redirect status/.test(e.message)) await route.continue({ url: resp.headers.location });
  else throw e;
}

Prevention

When it happens

Trigger: Calling route.fulfill({ status: 301/302/307/308, headers, body }) inside a route handler. Also fulfilling with a copied response object whose status happens to be 3xx.

Common situations: Replaying a recorded HAR/response that contained a redirect. Attempting to 'mock' a redirect by fulfilling with 302 + Location. Migrating from Chrome DevTools-style redirect mocking.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/2aff53426f6981eb. Report an issue: GitHub.