Contents
Introduction
This article explains, in full detail, how to refresh WordPress REST nonces without reloading the page, and how to wire that refresh into common client-side flows (fetch, jQuery, Axios). It includes server-side code to expose a nonce-refresh route, client-side patterns to consume it reliably, strategies to avoid race conditions, and security and caching considerations you must apply in production.
Background: what a WordPress REST nonce is and why you may need to refresh it
A WordPress nonce for REST API usage (the token typically passed in the X-WP-Nonce header) is a time-limited token that helps protect authenticated cookie-based REST calls from CSRF. By default the nonce has a finite lifetime (the default is 24 hours but can be changed via the nonce_life filter). If a user keeps a page open past that lifetime, REST calls will start failing with authentication errors (typically HTTP 401).
Reloading the whole page is one way to refresh the nonce (WordPress prints a new one into the page or into wpApiSettings), but this is disruptive to UX. Instead you can fetch a fresh nonce from the server and update the client-side state so subsequent REST calls succeed without a full page reload.
High-level approach
- Create a server endpoint that returns a fresh nonce. Prefer a REST route that requires the user to be logged in (permission callback) and that prevents caching.
- From JS, request that endpoint using same-origin credentials, store the returned nonce somewhere the rest of your client code will use, and ensure subsequent API calls include the new nonce in the X-WP-Nonce header.
- Wrap your fetch/jQuery/Axios logic so it will detect authentication failures (401), call the nonce-refresh endpoint, update the header, and retry the original request. Use concurrency-safe logic so simultaneous requests provoke only one refresh operation.
Server: create a secure REST endpoint that returns a fresh nonce
Use rest_api_init to register a route. Use a permission callback so only logged-in users can request nonces. Use nocache_headers() so intermediate caches/CDNs do not store the nonce response.
lt?php // Put this in your plugin file or theme functions.php add_action( rest_api_init, function() { register_rest_route( myplugin/v1, /nonce, array( methods => GET, callback => myplugin_get_rest_nonce, // Only allow logged-in users to request a nonce. permission_callback => function() { return is_user_logged_in() }, ) ) } ) function myplugin_get_rest_nonce( request ) { // Prevent caching of the response. nocache_headers() // Create a nonce for REST cookie authentication. wp_rest is the action used by WPs cookie auth. nonce = wp_create_nonce( wp_rest ) // Return a standard REST response object. return rest_ensure_response( array( nonce => nonce ) ) } ?gt
Notes:
- permission_callback: tailor checks for your needs. You can require capability checks like current_user_can(edit_posts) if only certain users should get a nonce.
- nocache_headers(): prevents CDN or browser caching of the nonce response.
- If you use admin-ajax.php instead of the REST route, the same principles apply (return a fresh wp_create_nonce and avoid caching), but REST routes are cleaner for REST-driven apps.
Client: basic fetch and update global wpApiSettings
A simple approach is to call the endpoint, extract the nonce, set window.wpApiSettings.nonce (WordPress uses that variable in many client libs), and update any AJAX global headers (jQuery/Axios) you rely upon.
// Basic: fetch a fresh nonce and apply it to common places (wpApiSettings, jQuery, global header) function applyNonce(nonce) { // Update the WP global so other code using wpApiSettings will see the new value. window.wpApiSettings = window.wpApiSettings {} window.wpApiSettings.nonce = nonce // If you use jQuery, update its default header. if ( window.jQuery ) { window.jQuery.ajaxSetup( { headers: { X-WP-Nonce: nonce } } ) } // Optionally store in a custom variable you use in your fetch wrappers. window.__myAppWpNonce = nonce } function refreshNonce() { return fetch( /wp-json/myplugin/v1/nonce, { credentials: same-origin, // send cookies for cookie auth headers: { Accept: application/json } } ).then( function( res ) { if ( ! res.ok ) { throw new Error( Failed to refresh nonce: res.status ) } return res.json() } ).then( function( json ) { // JSON format above: { nonce: ... } inside the returned object var nonce = json json.nonce ? json.nonce : (json json.data json.data.nonce ? json.data.nonce : null) if ( ! nonce ) { throw new Error( No nonce found in response ) } applyNonce( nonce ) return nonce } ) }
Client: intercepting fetch and retrying on 401 with concurrency protection
A robust pattern is to override or wrap window.fetch to automatically include the current nonce, detect 401 responses, call refreshNonce() and retry the original request once. Important: avoid triggering multiple concurrent refresh requests. Use a single shared promise for the currently pending refresh.
// Global state used for single-flight refresh var __myNonceRefreshPromise = null // Safe refresh that deduplicates concurrent calls. function refreshNonceOnce() { if ( __myNonceRefreshPromise ) { return __myNonceRefreshPromise } __myNonceRefreshPromise = refreshNonce().finally( function() { __myNonceRefreshPromise = null } ) return __myNonceRefreshPromise } // Keep original fetch reference var __originalFetch = window.fetch.bind( window ) window.fetch = function( input, init ) { init = init {} init.credentials = init.credentials same-origin init.headers = init.headers {} // If headers is a Headers instance, convert to plain object for mutation if ( typeof Headers !== undefined init.headers instanceof Headers ) { var headersObj = {} init.headers.forEach( function( v, k ) { headersObj[k] = v } ) init.headers = headersObj } // Inject nonce if available var currentNonce = ( window.wpApiSettings window.wpApiSettings.nonce ) ? window.wpApiSettings.nonce : window.__myAppWpNonce if ( currentNonce ) { init.headers[X-WP-Nonce] = currentNonce } return __originalFetch( input, init ).then( function( response ) { // If 401, try refreshing the nonce and retry once if ( response.status === 401 ) { // Optionally: check response body for WP-specific error codes first. return refreshNonceOnce().then( function( newNonce ) { init.headers[X-WP-Nonce] = newNonce return __originalFetch( input, init ) } ) } return response } ) }
Notes:
- This wrapper retries the request once after a successful nonce refresh. Do not loop infinitely on repeated 401s.
- Always use credentials: same-origin when relying on cookie authentication.
- Do not expose the nonce endpoint to unauthenticated users.
Client: jQuery AJAX flow with retry
If your app uses jQuery heavily, update ajaxSetup and wire a global error handler that triggers a refresh-and-retry for 401s.
// Set the header initially if you have a nonce if ( window.wpApiSettings window.wpApiSettings.nonce ) { jQuery.ajaxSetup( { headers: { X-WP-Nonce: window.wpApiSettings.nonce } } ) } // Single-flight refresher promise var __jqueryNonceRefresh = null function jqueryRefreshOnce() { if ( __jqueryNonceRefresh ) return __jqueryNonceRefresh __jqueryNonceRefresh = refreshNonce().finally( function() { __jqueryNonceRefresh = null } ) return __jqueryNonceRefresh } // Global AJAX error handler jQuery( document ).ajaxError( function( event, jqxhr, settings, thrownError ) { if ( jqxhr.status === 401 ) { // Attempt refresh and retry once jqueryRefreshOnce().then( function( newNonce ) { // Update header for retry var headers = settings.headers {} headers[X-WP-Nonce] = newNonce settings.headers = headers // Retry the original call jQuery.ajax( settings ) } ) } } )
Client: Axios interceptor example
If you use Axios, add an interceptor that injects the nonce and a response interceptor to refresh retry on 401. Use a single-flight refresh promise for concurrency safety.
// Create axios instance or use axios.defaults var axiosInstance = axios.create( { withCredentials: true // send cookies } ) // Request interceptor to add nonce header axiosInstance.interceptors.request.use( function( config ) { var nonce = ( window.wpApiSettings window.wpApiSettings.nonce ) ? window.wpApiSettings.nonce : window.__myAppWpNonce if ( nonce ) { config.headers[X-WP-Nonce] = nonce } return config } ) // Single-flight refresh var __axiosNonceRefresh = null function axiosRefreshOnce() { if ( __axiosNonceRefresh ) return __axiosNonceRefresh __axiosNonceRefresh = refreshNonce().finally( function() { __axiosNonceRefresh = null } ) return __axiosNonceRefresh } // Response interceptor to catch 401 and retry once axiosInstance.interceptors.response.use( function( resp ) { return resp }, function( error ) { var config = error.config if ( error.response error.response.status === 401 ! config.__isRetry ) { config.__isRetry = true return axiosRefreshOnce().then( function( nonce ) { config.headers[X-WP-Nonce] = nonce return axiosInstance( config ) } ) } return Promise.reject( error ) } )
Integration with WordPress built-in wpApiSettings
WordPress often exposes window.wpApiSettings.nonce. Many of the official JS libraries check that variable. If you update window.wpApiSettings.nonce as shown above, any third-party or WP core code that reads that variable after your update will use the new nonce. However, code that captured the previous value in a closure will still have the old one that is why using centralized wrappers (fetch wrapper, Axios instance, jQuery.ajaxSetup) is important.
Security and best practices
- Limit who can obtain nonces: Use permission_callback to ensure only authenticated users / allowed roles can request a new nonce. If a nonce endpoint is public, it undermines the purpose of the token model.
- No caching: Use nocache_headers() on the server and ensure your CDN or reverse proxy does not cache the response. CDN-cached nonces leak and become stale.
- HTTPS: Always use HTTPS. Nonces are sent in headers and should never be sent over plain HTTP.
- One retry only: Retry once after obtaining a new nonce. Do not loop retries indefinitely.
- Single-flight refresh: Deduplicate concurrent refreshes with a shared promise to avoid storming the server with multiple refresh requests when many client requests encounter expiration at the same time.
- Logging monitoring: Consider server-side logs to detect abnormal frequent nonce refreshes that could indicate abuse.
Common pitfalls and troubleshooting
Symptom | Likely cause | Fix |
---|---|---|
Fetch returns 401 even after refresh | Refresh returned a nonce but request still lacks correct cookie/session or permission or refresh endpoint returned nonce for a different user | Ensure cookies are sent (credentials: same-origin), ensure user session is valid, confirm permission_callback matches user capabilities. Verify WP checks X-WP-Nonce with action wp_rest. |
Multiple refresh calls triggered simultaneously | No single-flight guard many requests detected expiry at same time | Use single-flight promise (store a global refresh promise) so concurrent callers reuse it. |
Nonce endpoint response is cached | CDN or browser caching the nonce response | Call nocache_headers() on server and configure CDN to bypass caching for this route. Add no-store/no-cache headers. |
Client code uses old nonce because it captured value in closure | Some modules saved the nonce locally and do not read global state before each request | Centralize header injection in a wrapper/interceptor and update that wrapper to use latest global avoid storing nonce inside long-lived closures. |
Edge cases and additional considerations
- Multiple tabs: If the user has multiple tabs, each tab must refresh its own runtime state. You can use localStorage events to broadcast a new nonce across tabs, but be careful not to leak the nonce to unauthorized contexts. Using localStorage to share a nonce is acceptable in same-origin contexts still consider the security surface and prefer server-side checks.
- Session expiration vs nonce expiration: A 401 might mean the user session expired, not just the nonce. If refresh returns a nonce but requests still fail, check if the server session/cookie is still valid.
- CSRF vs authentication: Nonces mitigate CSRF for cookie-based auth. They do not replace authentication: endpoints must still check current user capabilities on the server.
- Nonce lifetime adjustments: If you need longer lifetimes, use the nonce_life filter however, longer lifetimes increase window where a stolen token could be abused.
Quick checklist to implement the pattern
- Register a REST route that returns wp_create_nonce(wp_rest) and uses nocache_headers() and an appropriate permission_callback.
- Expose a client-side function refreshNonce() that calls the route, extracts the nonce and updates window.wpApiSettings.nonce and your AJAX libraries headers.
- Wrap your HTTP client (fetch/jQuery/Axios) so it injects the current nonce and retries once after refresh if it sees a 401. Use a single-flight refresh promise so only one refresh happens at a time.
- Test flows: expired nonce, expired session, simultaneous requests, CDN caching.
- Harden: use HTTPS, no caching, restrict permission_callback, monitor abuse.
References
Summary
Refreshing REST nonces without a page reload is straightforward: provide a secure server endpoint that returns a fresh nonce, fetch it from JavaScript with credentials, update the client-side canonical nonce location(s), and wrap your HTTP client to automatically refresh and retry on 401. Follow the security and caching best practices in this article to avoid leaking tokens or creating stale cached responses.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |