Bryan Hamilton / projects / csrf-json-fetch

bryanhamilton.info / open source / csrf-json-fetch

csrf-json-fetch One token. Two injection points. One safe retry.

A synchronizer token sounds simple: fetch it, add it to the request, move on. Then two widgets fetch it at once, a session expires, or an endpoint expects it somewhere different. This zero-dependency helper handles those edges and powers the pattern behind POST requests on The DJ Calendar.

  • Shared token cache
  • Header + body injection
  • One 403 / 419 retry
  • Zero dependencies

Quick answer: csrf-json-fetch is a zero-dependency JavaScript helper for same-origin JSON POSTs. It shares CSRF token requests, injects the token into the request header and body, retries one 403/419 expiry response, and returns network failures as status 0. Project source on GitHub.

Break the request, then fix it

Mock transport only · nothing is sent over the network

The concurrent action runs two successful calls together so the single shared token request is visible. All tokens and responses are fake demo values.

Choose a response and run the simulation.

Request trace

Token and request events will appear here.

Resolved result

{
  "ok": null,
  "status": null,
  "data": null,
  "error": null
}
  1. 1 · ShareConcurrent calls reuse one token request.
  2. 2 · InjectHeader and JSON body both carry the token.
  3. 3 · Retry onceOnly 403 or 419 triggers one fresh-token retry.
  4. 4 · ResolveNetwork failures become status 0 results.

This is an explanatory simulation, not the library itself: no API, token endpoint or external service is contacted.

The token race and the stale session

A page loads, and two components POST at nearly the same time. If both independently ask for a CSRF token, both can work, but the browser has paid for two round-trips. A cached token plus one shared in-flight Promise means one request serves both callers.

Later, the visitor returns to a tab after the session has expired. The server answers with 419 (or 403); the client discards the stale token, gets a fresh one, and tries once more. A second expiry response is returned as an error instead of entering a retry loop.

The server gets the token both ways

Some endpoints validate X-CSRF-Token; others read csrf_token from the JSON. postJson includes both, so either server-side check can be used without changing the caller.

var headers = {
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'X-CSRF-Token': token
};

body.csrf_token = token;

return fetch(url, {
  method: 'POST',
  credentials: 'same-origin',
  headers: headers,
  body: JSON.stringify(body)
});

Retry expiry, not real errors

Only 403 and 419 trigger the token refresh path. A validation response such as 422 is returned directly with its JSON data intact; resending it would not fix invalid input.

Every path resolves to a result

Callers get the same four-field shape on success, HTTP errors, and failures to reach the server. Network, CORS, and timeout errors resolve with status 0; they do not escape as rejected promises.

Outcomestatusdataerror
Success200–299Response objectnull
HTTP error4xx / 5xxJSON object or nullhttp 422, etc.
Network / CORS / timeout0nullReadable error message

Use it in your project

Callers can handle success, validation feedback, and all other errors without wrapping every request in exception handling.

postJson('/api/subscribe', {
  email: 'dj@example.com',
  city: 'nyc'
}).then(function (res) {
  if (res.ok) {
    showSuccess(res.data.message);
  } else if (res.status === 422) {
    showErrors(res.data.errors);
  } else {
    showError(res.error);
  }
});

The defaults are /csrf-token.php, csrf_token, X-CSRF-Token, and expiry statuses 403 / 419. The request timeout defaults to 15 seconds and can be set per call.

Frequently asked questions

What does csrf-json-fetch do?

It posts JSON to same-origin APIs protected by synchronizer-token CSRF. Concurrent callers share token retrieval, and each request includes the token in both X-CSRF-Token and csrf_token.

When does csrf-json-fetch retry a request?

Only after HTTP 403 or 419, which the project treats as token-expiry responses. It refreshes the token and retries once; 422 validation errors and a second expiry response are not retried.

How does csrf-json-fetch report network errors?

Network, CORS, and timeout failures resolve as { ok: false, status: 0, data: null, error: "…" }; the helper does not throw for these failures.

Does the project page send real requests?

No. The project-page simulation uses local mock responses and sends no network requests. The library source uses fetch for actual same-origin API calls.

Need requests that fail gracefully?

I build small, production-ready browser tools that handle the edge cases without adding a framework or dependency pile.