Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: response.text()/.body() triggers remote request for fulfilled 404 #30760

Closed
pvdz opened this issue May 13, 2024 · 1 comment · Fixed by #30938
Closed

[Bug]: response.text()/.body() triggers remote request for fulfilled 404 #30760

pvdz opened this issue May 13, 2024 · 1 comment · Fixed by #30938

Comments

@pvdz
Copy link

pvdz commented May 13, 2024

Version

1.43.1

Steps to reproduce

  • see minimal test case, NodeJS
  • setup local webserver to verify remote requests (test case includes it)
  • create page with some resource fetched that you want to 404
  • await page.router() and intercept this request, mocking it with a 404 through fulfill()
  • observe (through log timing) that the local webserver is not hit until the await response.body()
  • then it will hit webserver
  • things to note:
    • same behavior for response.text()
    • content type of mocked data is not relevant
    • if status code of mocked data is 200, then remote is not hit
    • as an aside: actual webserver response seems irrelevant, same code, any valid code, invalid code, response text is not showing up anyways (not sure why the request is made at all)
    • observable headers are also not modified with remote data

Expected behavior

When I mock a request through fulfill I expect the remote not to be hit at all, regardless of what the mock gives it.

Actual behavior

Remote server is hit for unknown reasons.
Response appears to be ignored for insofar you can inspect it.

Additional context

Since the problem does not happen for status 200 responses it feels to me like it's somehow due to not expecting the 404 to have body contents? But that's just a guess.

Using DEBUG=pw:* does not really give me more information on why it might happen. It does reject invalid status codes returned by the webserver this way, but that does not seem to fail the response similar to how it otherwise would.

Environment

(envinfo complained that "no playwright preset found" but here's the default)

 System:
    OS: Linux 5.8 Ubuntu 20.04.1 LTS (Focal Fossa)
    CPU: (12) x64 Intel(R) Core(TM) i7-10710U CPU @ 1.10GHz
    Memory: 53.92 GB / 62.54 GB
    Container: Yes
    Shell: 5.0.17 - /bin/bash
  Binaries:
    Node: 20.12.2 - ~/.nvm/versions/node/v20.12.2/bin/node
    npm: 10.5.0 - ~/.nvm/versions/node/v20.12.2/bin/npm
    pnpm: 9.0.5 - ~/.local/share/pnpm/pnpm
  Managers:
    Apt: 2.0.2 - /usr/bin/apt
  Utilities:
    Make: 4.2.1 - /usr/bin/make
    GCC: 9.4.0 - /usr/bin/gcc
    Git: 2.25.1 - /usr/bin/git
    Curl: 7.68.0 - /usr/bin/curl
    OpenSSL: 1.1.1 - /usr/bin/openssl
  Virtualization:
    Docker: 24.0.5 - /usr/bin/docker
    Docker Compose: 1.25.0 - /usr/bin/docker-compose
  IDEs:
    Nano: 4.8 - /usr/bin/nano
  Languages:
    Bash: 5.0.17 - /usr/bin/bash
    Perl: 5.30.0 - /usr/bin/perl
    Python: 2.7.18 - /usr/bin/python
    Python3: 3.8.10 - /usr/bin/python3
  Browsers:
    Chrome: 86.0.4240.198

and

    "@sparticuz/chromium": "123.0.0",
    "playwright-core": "1.43.1",
    "typescript": "5.4.5",

Minimal test case:

const root = await startLocalServer();

const browser = await playwright.chromium.launch({
  executablePath: await chrome.executablePath(),
  headless: true, // use this instead of using chromium.headless because it uses the new `headless: "new"` which will throw because playwright expects `headless: boolean`
  args: [...chrome.args, '--hide-scrollbars', '--disable-web-security'],
});

const context = await browser.newContext();
const page = await context.newPage();

async function handler(response: Response) {
  console.log('on response start', response.url());
  console.log('on response status text:', response.statusText());
  console.log('on response headers:', await response.headersArray());
  console.log('on response text:', await response.body()); // <<-- this triggers a remote fetch, regardless. Does not happen when commented out.
  console.log('on response headers2:', await response.headersArray()); // Same as before
  console.log('on response finish');
}
page.on('response', response => {
  handler(response).then(() => console.log('handler completed...'), e => console.log('handler crashed:', e));
});

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

  if (url === `${root}/main`) {
    console.log('Root (remote) response:');
    const response = await route.fetch();
    return await route.fulfill({ response });
  }

  console.log('Fail (mock) response:', [url]);
  return route.fulfill({
    status: 404,
    contentType: 'text/plain',
    body: 'Not Found! (mocked)',
  });
});

await page.goto(`${root}/main`);

await page.waitForLoadState('networkidle');

console.log('Closing page');
await page.close();

async function startLocalServer(): Promise<string> {
  const PORT = 3022;
  const HOST = 'localhost';

  // Serve local website
  const server = http.createServer((req, res) => {
    console.log('server hit:', req.method, req.url);

    if (req.url === '/main') {
      res.statusCode = 200;
      res.setHeader('x-custom', 'stuff');
      res.end('<body>main response <script src="/fail"></script>');
      return;
    }

    res.statusCode = 404; // Can return any status code here, doesn't matter really
    res.setHeader('Content-Type', 'text/html'); // Content type doesn't appear to matter either
    res.setHeader('x-custom', 'stuff');
    res.end('This response text should not be fetched and/or displayed'); // At least this doesn't show up?
  });

  await new Promise(resolve => {
    server.listen(PORT, HOST, () => {
      resolve(undefined);
    });
  });

  return `http://${HOST}:${PORT}`;
}

Output:

Root (remote) response:
server hit: GET /main
on response start http://localhost:3022/main
on response status text: OK
on response headers: [
  { name: 'connection', value: 'close' },
  { name: 'content-length', value: '49' },
  { name: 'date', value: 'Mon, 13 May 2024 11:09:23 GMT' },
  { name: 'x-custom', value: 'stuff' }
]
Fail (mock) response: [ 'http://localhost:3022/fail' ]
on response text: <Buffer 3c 62 6f 64 79 3e 6d 61 69 6e 20 72 65 73 70 6f 6e 73 65 20 3c 73 63 72 69 70 74 20 73 72 63 3d 22 2f 66 61 69 6c 22 3e 3c 2f 73 63 72 69 70 74 3e>
on response headers2: [
  { name: 'connection', value: 'close' },
  { name: 'content-length', value: '49' },
  { name: 'date', value: 'Mon, 13 May 2024 11:09:23 GMT' },
  { name: 'x-custom', value: 'stuff' }
]
on response finish
handler completed...
on response start http://localhost:3022/fail
on response status text: Not Found
on response headers: [
  { name: 'content-length', value: '19' },
  { name: 'content-type', value: 'text/plain' }
]
server hit: GET /fail
on response text: <Buffer >
on response headers2: [
  { name: 'content-length', value: '19' },
  { name: 'content-type', value: 'text/plain' }
]
on response finish
handler completed...
Closing page
@mxschmitt
Copy link
Member

I was able to reproduce on Chromium, but not on WebKit/Firefox. Looks like a bug.

// @ts-check
import http from 'http';
import playwright from 'playwright';
const root = await startLocalServer();

const browser = await playwright.chromium.launch();

const context = await browser.newContext();
const page = await context.newPage();

async function handler(response) {
  console.log('on response start', response.url());
  console.log('on response status text:', response.statusText());
  console.log('on response headers:', await response.headersArray());
  console.log('on response text:', await response.body()); // <<-- this triggers a remote fetch, regardless. Does not happen when commented out.
  console.log('on response headers2:', await response.headersArray()); // Same as before
  console.log('on response finish');
}
page.on('response', response => {
  handler(response).then(() => console.log('handler completed...'), e => console.log('handler crashed:', e));
});

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

  if (url === `${root}/main`) {
    console.log('Root (remote) response:');
    const response = await route.fetch();
    return await route.fulfill({ response });
  }

  console.log('Fail (mock) response:', [url]);
  return route.fulfill({
    status: 404,
    contentType: 'text/plain',
    body: 'Not Found! (mocked)',
  });
});

await page.goto(`${root}/main`);

await page.waitForLoadState('networkidle');

console.log('Closing page');
await page.close();

async function startLocalServer() {
  const PORT = 3022;
  const HOST = 'localhost';

  // Serve local website
  const server = http.createServer((req, res) => {
    console.log('server hit:', req.method, req.url);

    if (req.url === '/main') {
      res.statusCode = 200;
      res.setHeader('x-custom', 'stuff');
      res.setHeader('Content-Type', 'text/html');
      res.end('<body>main response <script src="/fail"></script>');
      return;
    }

    res.statusCode = 404; // Can return any status code here, doesn't matter really
    res.setHeader('Content-Type', 'text/html'); // Content type doesn't appear to matter either
    res.setHeader('x-custom', 'stuff');
    res.end('This response text should not be fetched and/or displayed'); // At least this doesn't show up?
  });

  await new Promise(resolve => {
    server.listen(PORT, HOST, () => {
      resolve(undefined);
    });
  });

  return `http://${HOST}:${PORT}`;
}

Expected: No server hit: GET /fail
Actual: server hit: GET /fail

getting printed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants