⚡ NEW: Gallery - Full Gallery 2025

Cookie Store API

Living Standard — Last Updated

Participate:
GitHub whatwg/cookiestore (new issue, open issues)
Chat on Matrix
Commits:
GitHub whatwg/cookiestore/commits
Snapshot as of this commit
@cookiestoreapi
Tests:
web-platform-tests cookiestore/ (ongoing work)
Translations (non-normative):
简体中文
日本語
한국어

Abstract

An asynchronous JavaScript cookies API for documents and service workers.

1. Introduction

This section is non-normative.

This standard defines an asynchronous cookie API for scripts running in HTML documents and service workers.

HTTP cookies have, since their origins at Netscape (documentation preserved by archive.org), provided a valuable state-management mechanism for the web.

The synchronous single-threaded script-level document.cookie interface to cookies has been a source of complexity and performance woes further exacerbated by the move in many browsers from:

… to the modern web which strives for smoothly responsive high performance:

On the modern web a cookie operation in one part of a web application cannot block:

Newer parts of the web built in service workers need access to cookies too but cannot use the synchronous, blocking document.cookie interface at all as they both have no document and also cannot block the event loop as that would interfere with handling of unrelated events.

1.1. Alternative to document.cookie

Today writing a cookie means blocking your event loop while waiting for the browser to synchronously update the cookie jar with a carefully-crafted cookie string in Set-Cookie format:

document.cookie =
  '__Secure-COOKIENAME=cookie-value' +
  '; Path=/' +
  '; expires=Fri, 12 Aug 2016 23:05:17 GMT' +
  '; Secure' +
  '; Domain=example.org';
// now we could assume the write succeeded, but since
// failure is silent it is difficult to tell, so we
// read to see whether the write succeeded
var successRegExp =
  /(^|; ?)__Secure-COOKIENAME=cookie-value(;|$)/;
if (String(document.cookie).match(successRegExp)) {
  console.log('It worked!');
} else {
  console.error('It did not work, and we do not know why');
}

What if you could instead write:

const one_day_ms = 24 * 60 * 60 * 1000;
cookieStore.set(
  {
    name: '__Secure-COOKIENAME',
    value: 'cookie-value',
    expires: Date.now() + one_day_ms,
    domain: 'example.org'
  }).then(function() {
    console.log('It worked!');
  }, function(reason) {
    console.error(
      'It did not work, and this is why:',
      reason);
  });
// Meanwhile we can do other things while waiting for
// the cookie store to process the write...

This also has the advantage of not relying on document and not blocking, which together make it usable from service workers, which otherwise do not have cookie access from script.

This standard also includes a power-efficient monitoring API to replace setTimeout-based polling cookie monitors with cookie change observers.

1.2. Summary

In short, this API offers the following functionality:

1.3. Querying cookies

Both documents and service workers access the same query API, via the cookieStore property on the global object.

The get() and getAll() methods on CookieStore are used to query cookies. Both methods return Promises. Both methods take the same arguments, which can be either:

The get() method is essentially a form of getAll() that only returns the first result.

Reading a cookie:
try {
  const cookie = await cookieStore.get('session_id');
  if (cookie) {
    console.log(`Found ${cookie.name} cookie: ${cookie.value}`);
  } else {
    console.log('Cookie not found');
  }
} catch (e) {
  console.error(`Cookie store error: ${e}`);
}
Reading multiple cookies:
try {
  const cookies = await cookieStore.getAll('session_id'});
  for (const cookie of cookies)
    console.log(`Result: ${cookie.name} = ${cookie.value}`);
} catch (e) {
  console.error(`Cookie store error: ${e}`);
}

Service workers can obtain the list of cookies that would be sent by a fetch to any URL under their scope.

Read the cookies for a specific URL (in a service worker):
await cookieStore.getAll({url: '/admin'});

Documents can only obtain the cookies at their current URL. In other words, the only valid url value in Document contexts is the document’s URL.

The objects returned by get() and getAll() contain all the relevant information in the cookie store, not just the name and the value as in the older document.cookie API.

Accessing all the cookie data:
await cookie = cookieStore.get('session_id');
console.log(`Cookie scope - Domain: ${cookie.domain} Path: ${cookie.path}`);
if (cookie.expires === null) {
  console.log('Cookie expires at the end of the session');
} else {
  console.log(`Cookie expires at: ${cookie.expires}`);
}
if (cookie.secure)
  console.log('The cookie is restricted to secure origins');

1.4. Modifying cookies

Both documents and service workers access the same modification API, via the cookieStore property on the global object.

Cookies are created or modified (written) using the set() method.

Write a cookie:
try {
  await cookieStore.set('opted_out', '1');
} catch (e) {
  console.error(`Failed to set cookie: ${e}`);
}

The set() call above is shorthand for using an options dictionary, as follows:

await cookieStore.set({
  name: 'opted_out',
  value: '1',
  expires: null,  // session cookie

  // By default, domain is set to null which means the scope is locked at the current domain.
  domain: null,
  path: '/'
});

Cookies are deleted (expired) using the delete() method.

Delete a cookie:
try {
  await cookieStore.delete('session_id');
} catch (e) {
  console.error(`Failed to delete cookie: ${e}`);
}

Under the hood, deleting a cookie is done by changing the cookie’s expiration date to the past, which still works.

Deleting a cookie by changing the expiry date:
try {
  const one_day_ms = 24 * 60 * 60 * 1000;
  await cookieStore.set({
    name: 'session_id',
    value: 'value will be ignored',
    expires: Date.now() - one_day_ms });
} catch (e) {
  console.error(`Failed to delete cookie: ${e}`);
}

1.5. Monitoring cookies

To avoid polling, it is possible to observe changes to cookies.

In documents, change events are fired for all relevant cookie changes.

Register for change events in documents:
cookieStore.addEventListener('change', event => {
  console.log(`${event.changed.length} changed cookies`);
  for (const cookie in event.changed)
    console.log(`Cookie ${cookie.name} changed to ${cookie.value}`);

  console.log(`${event.deleted.length} deleted cookies`);
  for (const cookie in event.deleted)
    console.log(`Cookie ${cookie.name} deleted`);
});

In service workers, cookiechange events are fired against the global scope, but an explicit subscription is required, associated with the service worker’s registration.

Register for cookiechange events in a service worker:
self.addEventListener('activate', (event) => {
  event.waitUntil(async () => {
    // Snapshot current state of subscriptions.
    const subscriptions = await self.registration.cookies.getSubscriptions();

    // Clear any existing subscriptions.
    await self.registration.cookies.unsubscribe(subscriptions);

    await self.registration.cookies.subscribe([
      {
        name: 'session_id',  // Get change events for cookies named session_id.
      }
    ]);
  });
});

self.addEventListener('cookiechange', event => {
  // The event has |changed| and |deleted| properties with
  // the same semantics as the Document events.
  console.log(`${event.changed.length} changed cookies`);
  console.log(`${event.deleted.length} deleted cookies`);
});

Calls to subscribe() are cumulative, so that independently maintained modules or libraries can set up their own subscriptions. As expected, a service worker’s subscriptions are persisted for with the service worker registration.

Subscriptions can use the same options as get() and getAll(). The complexity of fine-grained subscriptions is justified by the cost of dispatching an irrelevant cookie change event to a service worker, which is much higher than the cost of dispatching an equivalent event to a Window. Specifically, dispatching an event to a service worker might require waking up the worker, which has a significant impact on battery life.

The getSubscriptions() allows a service worker to introspect the subscriptions that have been made.

Checking change subscriptions:
const subscriptions = await self.registration.cookies.getSubscriptions();
for (const sub of subscriptions) {
  console.log(sub.name, sub.url);
}

2. Concepts

A cookie is normatively defined for user agents by Cookies § User Agent Requirements.

Per Cookies § Storage Model, a cookie has the following fields: name, value, domain, path, http-only-flag.

To normalize a cookie name or value given a string input: remove all U+0009 TAB and U+0020 SPACE that are at the start or end of input.

A cookie is script-visible when it is in-scope and its http-only-flag is unset. This is more formally enforced in the processing model, which consults Cookies § Retrieval Model at appropriate points.

A cookie is also subject to certain size limits. Per Cookies § Storage Model:

Cookie attribute-values are stored as byte sequences, not strings.

A cookie store is normatively defined for user agents by Cookies § User Agent Requirements.

When any of the following conditions occur for a cookie store, perform the steps to process cookie changes.

2.3. Extensions to Service Workers

[Service-Workers] defines service worker registration, which this specification extends.

A service worker registration has an associated cookie change subscription list which is a list; each member is a cookie change subscription. A cookie change subscription is a tuple of name and url.

3. The CookieStore interface

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStore : EventTarget {
  Promise<CookieListItem?> get(USVString name);
  Promise<CookieListItem?> get(optional CookieStoreGetOptions options = {});

  Promise<CookieList> getAll(USVString name);
  Promise<CookieList> getAll(optional CookieStoreGetOptions options = {});

  Promise<undefined> set(USVString name, USVString value);
  Promise<undefined> set(CookieInit options);

  Promise<undefined> delete(USVString name);
  Promise<undefined> delete(CookieStoreDeleteOptions options);

  [Exposed=Window]
  attribute EventHandler onchange;
};

dictionary CookieStoreGetOptions {
  USVString name;
  USVString url;
};

enum CookieSameSite {
  "strict",
  "lax",
  "none"
};

dictionary CookieInit {
  required USVString name;
  required USVString value;
  DOMHighResTimeStamp? expires = null;
  USVString? domain = null;
  USVString path = "/";
  CookieSameSite sameSite = "strict";
  boolean partitioned = false;
  long long? maxAge = null;
};

dictionary CookieStoreDeleteOptions {
  required USVString name;
  USVString? domain = null;
  USVString path = "/";
  boolean partitioned = false;
};

dictionary CookieListItem {
  USVString name;
  USVString value;
};

typedef sequence<CookieListItem> CookieList;

3.1. The get() method

cookie = await cookieStore . get(name)
cookie = await cookieStore . get(options)

Returns a promise resolving to the first in-scope script-visible value for a given cookie name (or other options). In a service worker context this defaults to the path of the service worker’s registered scope. In a document it defaults to the path of the current document and does not respect changes from replaceState() or document.domain.

The get(name) method steps are:
  1. Let settings be this’s relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let list be the results of running query cookies with url and name.

    2. If list is failure, then reject p with a TypeError and abort these steps.

    3. If list is empty, then resolve p with null.

    4. Otherwise, resolve p with the first item of list.

  7. Return p.

The get(options) method steps are:
  1. Let settings be this’s relevant settings object.

  2. Let origin be settings’s origin.

  3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.

  4. Let url be settings’s creation URL.

  5. If options is empty, then return a promise rejected with a TypeError.

  6. If options["url"] exists:

    1. Let parsed be the result of parsing options["url"] with settings’s API base URL.

    2. If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments set to true, then return a promise rejected with a TypeError.

    3. If parsed’s origin and url’s origin are not the same origin, then return a promise rejected with a TypeError.

    4. Set url to parsed.

  7. Let p be a new promise.

  8. Run the following steps in parallel:

    1. Let list be the results of running query cookies with url and options["name"] with default null.

    2. If list is failure, then reject p with a TypeError and abort these steps.

    3. If list is empty, then resolve p with null.

    4. Otherwise, resolve p with the first item of list.