Skip to content

[Bug]: When aborting only first request with {times: 1} from page route then the second request randomly failed #41802

Closed

Description

@jesabot-cmyk

Version

1.56.1

Steps to reproduce

  1. Clone my repo at https://github.com/jesabot-cmyk/testPlaywrightIssue
  2. npm install
  3. then npm run test (it will perform 10 runs from playwirght config)
  4. You should see most of runs pass but a few are failing and falling to the hung issue

Here the minimal code to reproduce the bug and figure out what is happening:

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

test('route abort { times: 1 } - 2nd sequential fetch should not be affected', async ({ page }) => {
  await page.goto('/');

  await page.route('**/data', (route) => route.abort('timedout'), { times: 1 });

  const results = await page.evaluate(async () => {
    // Race each fetch against a 3s timeout to distinguish 'hung' from '200' or 'aborted'.
    // 'hung' = { times: 1 } captured the request but never resolved it (orphaned).
    async function fetchOrHung(url: string) {
      const timeout = new Promise<string>(resolve => setTimeout(() => resolve('hung'), 3000));
      const request = fetch(url).then(r => String(r.status)).catch(() => 'aborted');
      return Promise.race([request, timeout]);
    }
    const first = await fetchOrHung('/data');   // awaited — 2nd fetch only starts after 1st completes
    const second = await fetchOrHung('/data');
    return [first, second];
  });

  console.log('results:', results);
  // Expected: ['aborted', '200']
  // Bug:      ['aborted', 'hung']  ← { times: 1 } captured the 2nd request but orphaned it

  expect(results[0]).toBe('aborted');
  expect(results[1]).toBe('200');
});

Expected behavior

Request 1: is caught by the { times: 1 } handler and abort
Requests 2+: is not caught and succeed with status 200

Actual behavior

Request 1: is caught by the { times: 1 } handler and abort
Requests 2+: is caught and fail

Additional context

Random issue so many runs are required to reproduce and observe bug.
Workaround Applied (Stable): Replace { times: 1 } with manual flag-based counting:

let alreadyAborted = false;
await page.route('**/endpoint', async (route) => {
  if (alreadyAborted) {
    return route.continue();
  }
  alreadyAborted = true;
  return route.abort('timedout');
});

This workaround is stable and reliable.

Environment

 System:
    OS: Linux 6.18 Ubuntu 24.04.3 LTS 24.04.3 LTS (Noble Numbat)
    CPU: (32) x64 13th Gen Intel(R) Core(TM) i9-13950HX
    Memory: 19.74 GB / 31.19 GB
    Container: Yes
  Binaries:
    Node: 20.19.5 - /home/sabotje/.nvm/versions/node/v20.19.5/bin/node
    npm: 10.8.2 - /home/sabotje/.nvm/versions/node/v20.19.5/bin/npm
    pnpm: 9.15.9 - /home/sabotje/.nvm/versions/node/v20.19.5/bin/pnpm
  IDEs:
    VSCode: 1.128.1 - /home/sabotje/.vscode-server/bin/5264f2156cbcd7aea5fd004d29eaa10209155d66/bin/remote-cli/code
  Languages:
    Bash: 5.2.21 - /usr/bin/bash
  npmPackages:
    @playwright/test: ^1.56.1 => 1.61.1 

Activity

  1. dgozman commented on Jul 16, 2026

    @dgozman
    Collaborator

    Investigation notes: the problem seems to be in CDP, where calling Fetch.disable races with the just-issued request, and we only receive Network.requestWillBeSent event and nothing else.

    CDP log of a failing run (trimmed)
    SEND ► {"id":26,"method":"Fetch.enable","params":{"handleAuthRequests":true,"patterns":[{"urlPattern":"*","requestStage":"Request"}]}}
    
    // first fetch('/data') — intercepted and aborted, as expected
    ◀ RECV {"method":"Network.requestWillBeSent","params":{"requestId":"94050.2","request":{"url":"http://localhost:8907/data",...}}}
    ◀ RECV {"method":"Fetch.requestPaused","params":{"requestId":"interception-job-1.0","request":{"url":"http://localhost:8907/data",...},"networkId":"94050.2"}}
    SEND ► {"id":29,"method":"Fetch.failRequest","params":{"requestId":"interception-job-1.0","errorReason":"TimedOut"}}
    ◀ RECV {"id":29,"result":{}}
    
    // { times: 1 } handler expired, no routes left — Playwright disables interception
    SEND ► {"id":30,"method":"Network.setCacheDisabled","params":{"cacheDisabled":false}}
    SEND ► {"id":31,"method":"Fetch.disable"}
    
    // the abort unblocks the page script, which immediately starts the second fetch('/data')
    ◀ RECV {"method":"Network.loadingFailed","params":{"requestId":"94050.2","errorText":"net::ERR_TIMED_OUT",...}}
    ◀ RECV {"method":"Network.requestWillBeSent","params":{"requestId":"94050.3","request":{"url":"http://localhost:8907/data",...}}}
    ◀ RECV {"id":31,"result":{}}   // Fetch.disable acked only after the second request already started
    ◀ RECV {"id":30,"result":{}}
    
    // ...and that's it. No Fetch.requestPaused for 94050.3, no responseReceived, no loadingFailed — ever.
    ◀ RECV {"id":28,"result":{"result":{"type":"object","value":{"a":["aborted","hung"],"id":1}}}}  +2s
    
    Condensed repro test
    test('route abort with times: 1 should not affect second sequential fetch', async ({ page, server }) => {
      server.setRoute('/data', (req, res) => {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('ok');
      });
    
      await page.goto(server.EMPTY_PAGE);
      await page.route('**/data', route => route.abort('timedout'), { times: 1 });
    
      const results = await page.evaluate(async () => {
        async function fetchOrHung(url: string) {
          const timeout = new Promise<string>(resolve => setTimeout(() => resolve('hung'), 3000));
          const request = fetch(url).then(r => String(r.status)).catch(() => 'aborted');
          return Promise.race([request, timeout]);
        }
        const first = await fetchOrHung('/data');
        const second = await fetchOrHung('/data');
        return [first, second];
      });
    
      expect(results).toEqual(['aborted', '200']);
    });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions