cdpfleet Docs GitHub Dashboard

Cases / #57 · 2026-10-17 · Easy

Inspect and edit the cookie jar of a remote browser — and why WebKit refuses cookies on httpbin.org

Read the cookies a page would send, change a value and an expiry, delete one by name and carry the login into a new session as about 150 bytes of JSON. Then set cookies three ways on three hosts in Chromium, Firefox and WebKit: WebKit refuses every cookie on httpbin.org and postman-echo.com, which are on the Public Suffix List.

Chromium Firefox WebKit

Run on production on 2026-10-17: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

A scraper often needs to look inside the cookie jar: is the login cookie there, when does it expire, what did the consent banner set? Sometimes it has to change one — extend a session, switch a preference — or move a login from one run to the next without keeping a whole browser profile. Playwright's context cookie API does all of this over the remote connection. And when a cookie that works in Chromium silently doesn't stick in WebKit, it helps to know why.

What we used, and why

WhatWhy
context.cookies(url)The cookies that would be sent to that URL — domain, path and secure flag applied. Without a URL it returns every cookie in the context.
context.addCookies([...])Adds cookies, or replaces one with the same name, domain and path: the way to edit a value or an expiry. expires: -1 is a session cookie.
context.clearCookies({ name })Deletes only the matching cookies (name, domain and path filters, Playwright 1.43+); without a filter it empties the jar.
www.httpbin.org/cookiesEchoes the cookies the server received, so every change is checked from the site's side too.
proxyYour own proxy; a few KB of traffic.

How it works

  1. Chromium: the site sets session_token and theme (session cookies); a cookie for example.com is added too. Read the whole jar, then only the site's cookies.
  2. Edit: give session_token an expiry a week out and change theme to light by adding them again; the site echoes what it received.
  3. Delete theme by name, save the site's cookies as JSON and close the session.
  4. Launch a new session, add the saved cookies to an empty context and ask the site what it sees.
  5. In Chromium, Firefox and WebKit, set a cookie three ways — addCookies, a server Set-Cookie and document.cookie — on httpbin.org, www.httpbin.org and postman-echo.com, and read back what was kept.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL (your own proxy; a few KB of traffic)
import { chromium, firefox, webkit } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const PROXY = process.env.PROXY_URL;
const SITE = 'https://www.httpbin.org';

// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(engine, body) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://starter.cdpfleet.com/${engine}/session`, {
      method: 'POST',
      headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status !== 503 || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}

async function open(engine) {
  const session = await launch(engine, { proxy: PROXY, headless: true, record: false });
  const type = { chromium, firefox, webkit }[engine];
  return type.connect(session.wsUrl, { headers: { 'x-api-key': KEY } });
}

// httpbin answers 502/503 now and then: retry a page that isn't JSON.
async function json(page, url) {
  for (let attempt = 1; ; attempt++) {
    await page.goto(url, { timeout: 45000 });
    try { return JSON.parse(await page.textContent('body')); } catch (err) {
      if (attempt === 4) throw err;
      await new Promise((r) => setTimeout(r, 3000));
    }
  }
}
const names = (cookies) => cookies.map((c) => c.name).sort();

// Part 1: inspect, edit, clear and carry the jar (Chromium).
const jar = {};
let saved;
{
  const browser = await open('chromium');
  try {
    const context = await browser.newContext();
    const page = await context.newPage();
    // The site sets two cookies (no expiry: session cookies); another site sets one more.
    await json(page, `${SITE}/cookies/set?session_token=abc123&theme=dark`);
    await context.addCookies([{ name: 'other_site', value: '1', url: 'https://example.com' }]);
    jar.all_cookies = names(await context.cookies());
    const mine = await context.cookies(SITE); // only the cookies that would be sent to this URL
    jar.for_site = names(mine);
    jar.session_cookie_expires = mine.find((c) => c.name === 'session_token').expires; // -1 = session cookie

    // Edit: addCookies with the same name, domain and path replaces the cookie.
    const weekFromNow = Math.floor(Date.now() / 1000) + 7 * 86400;
    await context.addCookies([
      { ...mine.find((c) => c.name === 'session_token'), expires: weekFromNow },
      { ...mine.find((c) => c.name === 'theme'), value: 'light' },
    ]);
    const edited = await context.cookies(SITE);
    jar.token_expires_in_days = Math.round((edited.find((c) => c.name === 'session_token').expires - Date.now() / 1000) / 86400);
    jar.server_sees_after_edit = (await json(page, `${SITE}/cookies`)).cookies;

    // Clear one cookie by name (other filters: domain, path).
    await context.clearCookies({ name: 'theme' });
    jar.after_clear_by_name = names(await context.cookies());

    // Carry the login as plain JSON: just the site's cookies.
    saved = JSON.stringify(await context.cookies(SITE));
    jar.saved_json_bytes = saved.length;
  } finally {
    await browser.close();
  }
}
{
  const browser = await open('chromium'); // a new session on another browser
  try {
    const context = await browser.newContext();
    jar.new_session_before = names(await context.cookies());
    await context.addCookies(JSON.parse(saved));
    const page = await context.newPage();
    jar.new_session_server_sees = (await json(page, `${SITE}/cookies`)).cookies;
  } finally {
    await browser.close();
  }
}

// Part 2: the same three ways to set a cookie, on three hosts, in three engines.
// httpbin.org and postman-echo.com are on the Public Suffix List; www.httpbin.org isn't.
const HOSTS = ['httpbin.org', 'www.httpbin.org', 'postman-echo.com'];
const psl = [];
for (const engine of ['chromium', 'firefox', 'webkit']) {
  const browser = await open(engine);
  try {
    for (const host of HOSTS) {
      const context = await browser.newContext();
      const page = await context.newPage();
      const url = `https://${host}`;
      await context.addCookies([{ name: 'via_add_cookies', value: '1', url }]);
      await json(page, `${url}/cookies/set?via_set_cookie=1`); // Set-Cookie, then a redirect to /cookies
      await page.evaluate(() => { document.cookie = 'via_document_cookie=1; path=/; max-age=3600'; });
      const kept = names(await context.cookies(url));
      psl.push({
        engine,
        host,
        add_cookies: kept.includes('via_add_cookies'),
        set_cookie: kept.includes('via_set_cookie'),
        document_cookie: kept.includes('via_document_cookie'),
        server_sees: Object.keys((await json(page, `${url}/cookies`)).cookies).sort(),
      });
      await context.close();
    }
  } finally {
    await browser.close();
  }
}

console.log(JSON.stringify({ jar, psl }, null, 2));

What we got

EngineHostaddCookiesSet-Cookiedocument.cookieServer received
chromiumhttpbin.orgkeptkeptkept3 of 3
chromiumwww.httpbin.orgkeptkeptkept3 of 3
chromiumpostman-echo.comkeptkeptkept3 of 3
firefoxhttpbin.orgkeptkeptkept3 of 3
firefoxwww.httpbin.orgkeptkeptkept3 of 3
firefoxpostman-echo.comkeptkeptkept3 of 3
webkithttpbin.orgrefusedrefusedrefused0 of 3
webkitwww.httpbin.orgkeptkeptkept3 of 3
webkitpostman-echo.comrefusedrefusedrefused0 of 3

From the Node.js run on 2026-10-17. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
{
  "jar": {
    "all_cookies": [
      "other_site",
      "session_token",
      "theme"
    ],
    "for_site": [
      "session_token",
      "theme"
    ],
    "session_cookie_expires": -1,
    "token_expires_in_days": 7,
    "server_sees_after_edit": {
      "session_token": "abc123",
      "theme": "light"
    },
    "after_clear_by_name": [
      "other_site",
      "session_token"
    ],
    "saved_json_bytes": 151,
    "new_session_before": [],
    "new_session_server_sees": {
      "session_token": "abc123"
    }
  },
  "psl": [
    {
      "engine": "chromium",
      "host": "httpbin.org",
      "add_cookies": true,
      "set_cookie": true,
      "document_cookie": true,
      "server_sees": [
        "via_add_cookies",
        "via_document_cookie",
        "via_set_cookie"
      ]
    },
    {
      "engine": "chromium",
      "host": "www.httpbin.org",
      "add_cookies": true,
      "set_cookie": true,
      "document_cookie": true,
      "server_sees": [
        "via_add_cookies",
        "via_document_cookie",
        "via_set_cookie"
      ]
    },
    {
      "engine": "chromium",
      "host": "postman-echo.com",
      "add_cookies": true,
      "set_cookie": true,
      "document_cookie": true,
      "server_sees": [
        "__cf_bm",
        "_cfuvid",
        "via_add_cookies",
        "via_document_cookie",
        "via_set_cookie"
      ]
    },
    {
      "engine": "firefox",
      "host": "httpbin.org",
      "add_cookies": true,
      "set_cookie": true,
      "document_cookie": true,
      "server_sees": [
        "via_add_cookies",
        "via_document_cookie",
        "via_set_cookie"
      ]
    },
    {
      "engine": "firefox",
      "host": "www.httpbin.org",
      "add_cookies": true,
      "set_cookie": true,
      "document_cookie": true,
      "server_sees": [
        "via_add_cookies",
        "via_document_cookie",
        "via_set_cookie"
      ]
    },
    {
      "engine": "firefox",
      "host": "postman-echo.com",
      "add_cookies": true,
      "set_cookie": true,
      "document_cookie": true,
      "server_sees": [
        "__cf_bm",
        "_cfuvid",
        "via_add_cookies",
        "via_document_cookie",
        "via_set_cookie"
      ]
    },
    {
      "engine": "webkit",
      "host": "httpbin.org",
      "add_cookies": false,
      "set_cookie": false,
      "document_cookie": false,
      "server_sees": []
    },
    {
      "engine": "webkit",
      "host": "www.httpbin.org",
      "add_cookies": true,
      "set_cookie": true,
      "document_cookie": true,
      "server_sees": [
        "via_add_cookies",
        "via_document_cookie",
        "via_set_cookie"
      ]
    },
    {
      "engine": "webkit",
      "host": "postman-echo.com",
      "add_cookies": false,
      "set_cookie": false,
      "document_cookie": false,
      "server_sees": []
    }
  ]
}

Takeaways