How to check if third party cookies are blocked

Load a page that makes a cross-site request and read Network.responseReceivedExtraInfo for the blocked cookies. In a fresh Chrome 152 profile nothing was blocked. With third-party restriction on, the same SameSite=None; Secure cookie came back with reason ThirdPartyPhaseout, while the Partitioned cookie was still set and still sent.

Why check this

Every answer to this question that reads document.cookie is guessing. A missing cookie has at least four causes: the browser blocked it, the attribute combination was invalid, the path did not match, or the response never set it. Only the browser knows which, and it says so in the protocol.

Run this whenever a login, a payment step or an embedded widget crosses a site boundary, and again after any browser update. The failure it catches is an embedded checkout that works on your machine and signs users out on a colleague's, because one profile restricts third-party cookies and the other does not.

Prerequisites

// tpc.mjs   the third party. It sets three cookies and reports what came back.
import { createServer } from 'node:http';
const SET = [
  'tp_none=1; Path=/; SameSite=None; Secure',
  'tp_lax=1; Path=/; SameSite=Lax',
  'tp_chips=1; Path=/; SameSite=None; Secure; Partitioned',
];
createServer((req, res) => {
  const path = req.url.split('?')[0];
  console.log(JSON.stringify({ url: req.url, cookie: req.headers.cookie ?? null }));
  if (path === '/set') {
    res.writeHead(204, { 'set-cookie': SET, 'cache-control': 'no-store' });
    return res.end();
  }
  if (path === '/frame') {
    res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
    return res.end(`<!doctype html><meta charset="utf-8"><script>
      parent.postMessage({ frameCookie: document.cookie }, '*');</script>`);
  }
  res.writeHead(204, { 'cache-control': 'no-store' });
  res.end();
}).listen(9625, '127.0.0.1', () => console.log('third party listening on 127.0.0.1:9625'));
// main.mjs   the top-level page: set, then ping, then read document.cookie inside an iframe
import { createServer } from 'node:http';
const T = 'http://127.0.0.1:9625';
const page = `<!doctype html><meta charset="utf-8"><title>Third party cookies</title>
<h1>Third party cookie fixture</h1>
<script>
  window.result = (async () => {
    await fetch('${T}/set', { mode: 'no-cors', credentials: 'include' });
    await fetch('${T}/ping', { mode: 'no-cors', credentials: 'include' });
    const frameCookie = await new Promise((ok) => {
      addEventListener('message', (e) => ok(e.data.frameCookie), { once: true });
      const f = document.createElement('iframe');
      f.src = '${T}/frame';
      document.body.append(f);
    });
    return { frameCookie };
  })();
</script>`;
createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
  res.end(page);
}).listen(9624, 'localhost', () => console.log('main site listening on localhost:9624'));
// cookies.mjs   loads the fixture and reports every cookie decision Chrome made
import { open } from '../../scripts/browser/session.mjs';

const extra = process.argv.slice(2);
const s = await open({ args: extra });
try {
  console.log('chrome ' + (await s.browser.version()) + (extra.length ? '  flags: ' + extra.join(' ') : '  flags: (none)'));
  await s.cdp.send('Network.enable');
  const respExtra = [];
  const reqExtra = [];
  s.cdp.on('Network.responseReceivedExtraInfo', (e) => respExtra.push(e));
  s.cdp.on('Network.requestWillBeSentExtraInfo', (e) => reqExtra.push(e));
  const urls = new Map();
  s.cdp.on('Network.requestWillBeSent', (e) => urls.set(e.requestId, e.request.url));

  await s.goto('http://localhost:9624/');
  console.log('frame read of document.cookie: ' + JSON.stringify(await s.page.evaluate(() => window.result)));

  console.log('\n--- Set-Cookie, as Chrome judged it ---');
  for (const e of respExtra) {
    const u = urls.get(e.requestId) ?? '(unknown)';
    if (!u.includes('9625')) continue;
    for (const c of e.blockedCookies ?? [])
      console.log(`blocked  ${c.cookieLine}\n         reasons: ${JSON.stringify(c.blockedReasons)}`);
    for (const c of e.exemptedCookies ?? [])
      console.log(`exempted ${c.cookie.name}  reason: ${c.exemptionReason}`);
    if (e.cookiePartitionKey) console.log(`partitionKey ${JSON.stringify(e.cookiePartitionKey)}`);
  }

  console.log('\n--- cookies on the outgoing request to /ping ---');
  for (const e of reqExtra) {
    const u = urls.get(e.requestId) ?? '';
    if (!u.includes('/ping')) continue;
    for (const c of e.associatedCookies ?? [])
      console.log(`${c.blockedReasons?.length ? 'not sent' : 'sent    '} ${c.cookie.name}=${c.cookie.value}` +
        `  partitionKey=${c.cookie.partitionKey ? JSON.stringify(c.cookie.partitionKey) : 'none'}` +
        `${c.blockedReasons?.length ? '  reasons: ' + JSON.stringify(c.blockedReasons) : ''}`);
  }

  console.log('\n--- the cookie jar ---');
  const { cookies } = await s.cdp.send('Storage.getCookies');
  for (const c of cookies)
    console.log(`${c.name}=${c.value}  domain=${c.domain}  sameSite=${c.sameSite ?? 'unset'}  secure=${c.secure}  partitionKey=${c.partitionKey ? JSON.stringify(c.partitionKey) : 'none'}`);
} finally { await s.close(); }

Steps

  1. Step 1.

    Run the fixture in a fresh profile with no flags, which is the browser's own default.

    node cookies.mjs
    
    chrome Chrome/152.0.7977.76  flags: (none)
    frame read of document.cookie: {"frameCookie":"tp_none=1; tp_chips=1"}
    
    --- Set-Cookie, as Chrome judged it ---
    blocked  tp_lax=1; Path=/; SameSite=Lax
           reasons: ["SchemefulSameSiteLax"]
    partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    
    --- cookies on the outgoing request to /ping ---
    sent     tp_none=1  partitionKey=none
    sent     tp_chips=1  partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    
    --- the cookie jar ---
    tp_none=1  domain=127.0.0.1  sameSite=None  secure=true  partitionKey=none
    tp_chips=1  domain=127.0.0.1  sameSite=None  secure=true  partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}

    Third-party cookies are not blocked in this profile. The only rejection is tp_lax, and its reason is SchemefulSameSiteLax: a SameSite=Lax cookie cannot be set from a cross-site response at all, restriction or no restriction. That row would appear on a browser with every privacy setting off.

  2. Step 2.

    Read the same visit from the server, which sees only what arrived.

    cat tpc.log
    
    third party listening on 127.0.0.1:9625
    {"url":"/set","cookie":null}
    {"url":"/ping","cookie":"tp_none=1; tp_chips=1"}
    {"url":"/frame","cookie":"tp_none=1; tp_chips=1"}

    Two of the three cookies came back on the next cross-site request and on the iframe. The server cannot tell why the third is missing. That is the whole reason the check runs in the browser.

  3. Step 3.

    Run the same fixture with third-party cookie restriction turned on.

    node cookies.mjs --test-third-party-cookie-phaseout
    
    chrome Chrome/152.0.7977.76  flags: --test-third-party-cookie-phaseout
    frame read of document.cookie: {"frameCookie":"tp_chips=1"}
    
    --- Set-Cookie, as Chrome judged it ---
    blocked  tp_none=1; Path=/; SameSite=None; Secure
           reasons: ["ThirdPartyPhaseout"]
    blocked  tp_lax=1; Path=/; SameSite=Lax
           reasons: ["UserPreferences"]
    partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    
    --- cookies on the outgoing request to /ping ---
    sent     tp_chips=1  partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}
    
    --- the cookie jar ---
    tp_chips=1  domain=127.0.0.1  sameSite=None  secure=true  partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}

    tp_none is now blocked with ThirdPartyPhaseout. tp_chips carries Partitioned, so it is set, stored with a partition key and sent on the next request. Partitioned is not a milder form of blocked. It is a separate jar, one per top-level site, and the vendor cannot join it to what it saw on another site.

  4. Step 4.

    Compare the two blocked reasons for tp_lax, which never changed.

    node cookies.mjs --test-third-party-cookie-phaseout | grep -A1 tp_lax
    
    blocked  tp_lax=1; Path=/; SameSite=Lax
           reasons: ["UserPreferences"]

    The identical cookie line was rejected as SchemefulSameSiteLax in step 1 and as UserPreferences in step 3. The restriction check runs first and reports its own reason, so the attribute problem underneath it disappears from the output. A test that asserts on one reason string passes or fails depending on a browser setting that has nothing to do with the bug.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | No blocked lines for your cookie | This profile allows third-party cookies | Repeat with restriction on before calling the feature safe. | | reasons: ["ThirdPartyPhaseout"] | The browser restricted it as a third-party cookie | Add Partitioned, or move the state to the top-level site. | | reasons: ["UserPreferences"] | A profile setting blocked it, ahead of any other check | Do not read the attribute set from this. Re-run with defaults. | | reasons: ["SchemefulSameSiteLax"] | The cookie was never valid cross-site | Fix the attributes. No privacy setting is involved. | | partitionKey on the cookie in the jar | A CHIPS cookie, stored per top-level site | Confirm the vendor can work without joining across sites. | | partitionKey in the response line only | The partition context of the request | Ignore it. It says nothing about the cookie. |

Common mistakes

Sign: An automated test asserts on the blocked reason and fails after a browser setting changes.Cause: The reason is whichever check rejected the cookie first. The same SameSite=Lax cookie line reported SchemefulSameSiteLax with defaults and UserPreferences under restriction, in two runs of one fixture on one machine. Assert that the cookie is absent, and read the reason as a diagnosis, not as a contract.
Sign: The cookie jar is read after the load and the cookie is not there.Cause: A jar holds survivors. It cannot separate blocked from never set, from wrong path, from a response that did not include it. The blocked list arrives on the response event and is gone once the load is over, so capture it during the navigation or you lose the answer.
Sign: A partitioned cookie is reported as evidence that restriction is off.Cause: CHIPS cookies survive third-party restriction by design. In the restricted run tp_chips was set and sent while tp_none was blocked. Check the cookie's own partitionKey in the jar: none means a classic third-party cookie, a value means a separate store per top-level site.
Sign: An iframe reads document.cookie and shows nothing, so third-party cookies look blocked.Cause: An HttpOnly cookie is invisible to that read, and so is any cookie whose path does not match the frame URL. The frame read here agreed with the protocol because the fixture sets neither. On a real vendor frame it will not.

What to check next

FAQ

How to check third party cookies without writing a fixture?

Open DevTools, Network tab, select a cross-site request, and read the Cookies panel. Rows that were rejected are shown with a reason and a warning triangle. The fixture exists so the answer is reproducible, not because the panel is wrong.

How to check if third party cookies are enabled in this browser?

Run a cross-site request and look for a ThirdPartyPhaseout or UserPreferences entry in the blocked list. Anything shorter, including a settings screenshot, describes the configuration rather than the behaviour, and enterprise policy and Incognito both override the setting.

How to check if third party cookies are enabled with JavaScript?

Embed an iframe from another site that sets a cookie and reports document.cookie back through postMessage, as main.mjs does. Treat the result as a signal, not proof: HttpOnly and a mismatched path both produce the same empty string.

How to check third party cookies in Chrome versus Edge?

The method is identical, because both expose the same protocol. The verdicts are not. Blocking is a browser and profile property, so a matrix of the browsers your users have is the only useful result.

Does the Partitioned attribute mean the cookie is blocked?

No. It is stored in a separate jar per top-level site. In the restricted run the partitioned cookie was set and sent while the plain one was blocked, so a single site keeps working and cross-site joining stops.

Verified

Verified by Maks Vernynode 22.23.2Chrome 152.0.7977.76

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

advanced15 minpublished updated Maks Verny