Guide 01 · Developer docs

Choosing how to build

Decide where the pages of a Core dna site are composed before writing code: on the platform, in your own application, or on the platform with a few pieces filled in by the browser. The decision tree, each shape in depth, multi-brand and languages, and a table and case studies to make the call.

L2
Unit 04 · Hybrid, the common case

One client module, carrying the CSRF token

The CSRF token, the base URL and the headers live in one module, and every request goes through it. The module, where the token comes from on cached and uncached pages, and the failure it prevents: some requests answering 403 while others work.

Who this is for. The JavaScript developer on a hybrid site whose scripts write to the platform: a cart, a checkout, a form, a member's settings. After this page every request your scripts send is built in one place, and a missing token cannot hide in one call site.

When the platform checks the token

The platform checks a CSRF token on a request only when all three are true: someone is signed in, the method is POST, PUT, PATCH or DELETE, and the request carries no Authorization header. That is every write a signed-in member's browser makes with its session cookie. The token is sent as an X-Csrf-Token header, or as a _csrf field in the body. The checkout routes check it; the cart and form-submission routes do not.

Without the token, the answer is a 403 with Missing or invalid CSRF token. Whether that arrives as JSON depends on the request: with Accept: application/json the platform answers with JSON, and without it with its HTML error page, which response.json() cannot parse. Read from the platform's source:

HTTP A write without the token, from a signed-in session
HTTP/1.1 403 Forbidden
Content-Type: application/json

{"result":"error","message":"Missing or invalid CSRF token"}

The module

One file owns the token, the headers and the parsing of answers. Nothing else in the site calls fetch on the platform:

JavaScript src/js/api-client.js
let csrfToken = window.csrfToken || '';

// A fragment that brings a fresh token calls this; no other code knows the token exists.
export function setCsrfToken(token) {
  csrfToken = token || '';
}

export async function api(path, { method = 'GET', body } = {}) {
  const headers = { Accept: 'application/json' };
  if (body !== undefined) headers['Content-Type'] = 'application/json';
  if (method !== 'GET' && csrfToken) headers['X-Csrf-Token'] = csrfToken;

  const response = await fetch(path, {
    method,
    headers,
    credentials: 'same-origin',
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  // Read the body as text first: an empty answer or an HTML error page must not throw here.
  const text = await response.text();
  let data = null;
  try {
    data = text ? JSON.parse(text) : null;
  } catch {
    data = { result: 'error', message: text.slice(0, 200) };
  }
  return { status: response.status, ok: response.ok, data };
}

The failure it prevents

When each script builds its own requests, the token is found a different way in each: one reads a meta tag, one a hidden input, one a global. They all work while the token is in the place they expect. Then the page becomes cacheable, the token moves into a fragment, and a subset of writes starts answering 403 while the rest work. It looks random, it happens only to signed-in members, and it never shows up in tests run with an API key, which the check does not apply to. With one module, the move is one line: the fragment calls setCsrfToken().

Where the token comes from

PageHow the token reaches the module
A page that is never cached, such as the checkout or the account areaThe template writes _csrfToken into the page, as window.csrfToken (Handing state to the browser) or a <meta> tag
A cached pageNowhere in the page: it was rendered for a visitor who was not signed in, so it has no token. The account fragment brings it (The fragment template)
A form posted the ordinary wayA hidden _csrf field, below
Template code
{if isset($_csrfToken)}<input type="hidden" name="_csrf" value="{$_csrfToken}">{/if}
{% if _csrfToken is defined %}<input type="hidden" name="_csrf" value="{{ _csrfToken }}">{% endif %}

The token belongs to the session, so it changes when the session does. A token that worked an hour ago and fails now usually means the member signed in again in another tab: reload the page rather than retry the request. On the server side of a hybrid site, scripts and integrations use an OAuth token or an API key instead, and the check does not apply to them.

Go deeper

Was this page helpful?

Your answer goes to the docs team.