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.
- First 403 / 419: drop the cached token, fetch a fresh token, retry once.
- Second 403 / 419: return the server response; do not loop.
- 422 and other HTTP errors: return the response without retrying.
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.
| Outcome | status | data | error |
|---|---|---|---|
| Success | 200–299 | Response object | null |
| HTTP error | 4xx / 5xx | JSON object or null | http 422, etc. |
| Network / CORS / timeout | 0 | null | Readable 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.