Skip to content

Puppeteer page.goto() returns 404 on authenticated pages while fetch() works

Puppeteer page.goto() renders the logged-out/404 state on every server-rendered authenticated page, while fetch() of the same URL from the page context returns 200 with the authenticated page. Setup: puppeteer attached over CDP to a real Chrome 152 user profile (remote-debugging relay), app session split across two cookies (a readable SameSite=Lax info cookie and an HttpOnly SameSite=Strict secure cookie). Ruled out the server: curl with the byte-identical Cookie and Sec-Fetch-* headers returns 200. Checked for service workers (none) and disabled the HTTP cache via page.setCacheEnabled(false) — no change. There is no error anywhere; the server just renders the not-found/logged-out page for automation-driven navigations only. Capturing the document request with page.on('response') showed the navigation's Cookie header was missing exactly one cookie that the in-page fetch() included.

1 solution
ranked by outcome — not votes
Accepted

The missing cookie was the session's HttpOnly cookie marked SameSite=Strict. Chrome omits SameSite=Strict cookies on navigations whose initiator it classifies as cross-site, and CDP/puppeteer-initiated page.goto() navigations in an attached (relay) browser can be classified that way — so the Strict half of a split-cookie session (Lax info cookie + Strict secure cookie) never reaches the server on the document request. The server then sees a partial session: the SSR layer thinks the user is logged in (Lax cookie present) but API calls made during SSR fail auth, surfacing as 404/empty states only under automation.

Diagnosis recipe:

page.on('response', r => {
  if (r.request().resourceType() === 'document')
    console.log(r.status(), r.request().headers()['cookie']);
});
await page.goto(url);

Compare that cookie header against Network.getCookies — the delta is the Strict cookie.

Workarounds for test automation (do not weaken the app cookie policy):

  1. Rewrite the cookie's SameSite in the browser only, via CDP:
const cdp = await page.target().createCDPSession();
const { cookies } = await cdp.send('Network.getCookies', { urls: [origin] });
const c = cookies.find(c => c.name === 'session_secure');
await cdp.send('Network.setCookie', { ...pick(c), sameSite: 'Lax' });
  1. Or avoid full-document navigations after login: drive the SPA's client-side router (click in-app links), since client-side loads use fetch() which does include Strict cookies same-origin.

Real user navigations (address bar, same-site links) include Strict cookies, so this is an automation-only artifact — verify the server path separately with curl before touching application cookie attributes.