Khi nào lỗi này xuất hiện
Bạn đang gọi WordPress REST API từ script của theme, một custom block, hoặc frontend headless — và thay vì nhận được dữ liệu, bạn nhận được lỗi 403:
{ "code": "rest_cookie_invalid_nonce", "message": "Cookie nonce is invalid", "data": { "status": 403 } }
Người dùng đã đăng nhập. Cookie vẫn còn đó. Nhưng WordPress vẫn từ chối request. Nonce được gửi kèm trong API call của bạn đã không qua được bước xác thực ở phía server.
Nguyên nhân gây lỗi
Xác thực bằng cookie cho WordPress REST API yêu cầu một action nonce cụ thể: wp_rest. Với mỗi request có xác thực, WordPress kiểm tra token này để đảm bảo request xuất phát từ phiên đăng nhập thực sự — chứ không phải một cross-site request giả mạo. Có nhiều nguyên nhân khiến bước kiểm tra này thất bại:
- Sai action nonce — lỗi phổ biến nhất.
wp_create_nonce('bất_kỳ_thứ_gì_khác')tạo ra hash sai. Action phải là chính xácwp_rest. - Nonce bị cache — plugin caching phục vụ HTML cũ chứa nonce đã hết hạn từ nhiều giờ trước. Nonce có thời hạn 12–24 giờ, nhưng các trang được cache có thể phục vụ chúng lâu hơn nhiều.
- Thiếu cờ credentials — không có
credentials: 'include'trong fetch call đồng nghĩa với việc trình duyệt không bao giờ gửi cookie. Khi đó việc kiểm tra nonce sẽ thất bại dù hash có đúng hay không. - Không gửi nonce — header
X-WP-Noncehoặc query parameter_wpnoncehoàn toàn vắng mặt trong request. - Phiên đăng nhập hết hạn — phiên đăng nhập của người dùng đã timeout, khiến tất cả các nonce gắn với phiên đó bị vô hiệu hóa.
Cách sửa nhanh
Bước 1 — Dùng đúng action nonce
Trong functions.php hoặc file plugin của bạn, action phải là wp_rest — không phải slug plugin, không phải chuỗi tùy chỉnh:
// Đúng
$nonce = wp_create_nonce( 'wp_rest' );
// Sai — gây ra rest_cookie_invalid_nonce
$nonce = wp_create_nonce( 'my_plugin_action' );
Bước 2 — Truyền nonce vào JavaScript bằng wp_localize_script
Hook vào wp_enqueue_scripts để truyền cả nonce lẫn REST root URL đến frontend:
function my_enqueue_scripts() {
wp_enqueue_script(
'my-app',
get_template_directory_uri() . '/js/app.js',
[],
'1.0',
true
);
wp_localize_script( 'my-app', 'wpApiSettings', [
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
] );
}
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );
Bước 3 — Gửi nonce trong fetch call
Có hai điều quan trọng ở đây: header X-WP-Nonce mang token xác thực, và credentials: 'include' yêu cầu trình duyệt gửi kèm auth cookie. Thiếu một trong hai là bạn sẽ nhận ngay lỗi 403.
fetch( wpApiSettings.root + 'wp/v2/posts', {
method: 'GET',
credentials: 'include', // bắt buộc — gửi auth cookie
headers: {
'X-WP-Nonce': wpApiSettings.nonce,
'Content-Type': 'application/json',
},
} )
.then( res => res.json() )
.then( data => console.log( data ) );
Muốn dùng query string thay vì header? Nối thêm ?_wpnonce= vào URL:
const url = wpApiSettings.root + 'wp/v2/posts?_wpnonce=' + wpApiSettings.nonce;
fetch( url, { credentials: 'include' } );
Giải pháp lâu dài — xử lý nonce hết hạn một cách linh hoạt
Một nonce được nhúng vào trang lúc tải sẽ có lúc hết hạn. Các ứng dụng single-page và phiên làm việc kéo dài thường xuyên gặp vấn đề này — người dùng để tab mở qua đêm sẽ quay lại với một nonce cũ không còn dùng được. Hãy đăng ký một AJAX endpoint để cấp nonce mới theo yêu cầu:
// functions.php
add_action( 'wp_ajax_wp_rest_nonce', function() {
wp_die( wp_create_nonce( 'wp_rest' ) );
} );
Sau đó bọc tất cả các fetch call vào một hàm helper tự động bắt lỗi 403 do nonce hết hạn và thử lại:
async function apiFetch( url, options = {} ) {
const defaultHeaders = {
'X-WP-Nonce': wpApiSettings.nonce,
'Content-Type': 'application/json',
};
const response = await fetch( url, {
credentials: 'include',
...options,
headers: { ...defaultHeaders, ...( options.headers || {} ) },
} );
if ( response.status === 403 ) {
const body = await response.json();
if ( body.code === 'rest_cookie_invalid_nonce' ) {
const refreshRes = await fetch(
'/wp-admin/admin-ajax.php?action=wp_rest_nonce',
{ credentials: 'include' }
);
const newNonce = await refreshRes.text();
wpApiSettings.nonce = newNonce;
return fetch( url, {
credentials: 'include',
...options,
headers: { ...defaultHeaders, 'X-WP-Nonce': newNonce, ...( options.headers || {} ) },
} );
}
}
return response;
}
Thay tất cả các lệnh fetch() trực tiếp bằng apiFetch() và bạn sẽ không còn phải lo về việc nonce hết hạn nữa.
Xử lý với plugin caching
WP Rocket, W3 Total Cache, LiteSpeed Cache — tất cả đều cache toàn bộ HTML của trang, bao gồm cả những gì wp_localize_script đã nhúng vào. Người truy cập vào trang được cache sẽ nhận được một nonce có thể đã cũ hàng giờ và không còn hợp lệ. Có ba cách xử lý:
- Loại trang khỏi cache — hầu hết các plugin đều cho phép bạn chặn cache cho URL cụ thể, hoặc bỏ qua cache hoàn toàn với người dùng đã đăng nhập.
- Dùng pattern làm mới nonce ở trên — tải nonce mới trước lần gọi API thực sự đầu tiên thay vì tin vào nonce đã nhúng sẵn trong trang.
- Giảm thời gian cache TTL — giới hạn ở mức 1–2 giờ với bất kỳ trang nào thực hiện các REST API call có xác thực.
Nếu không còn cách nào khác, bạn có thể kéo dài thời hạn nonce ở phía server để có thêm thời gian giữa các lần hết hạn:
add_filter( 'nonce_life', function() {
return 24 * HOUR_IN_SECONDS; // 24 giờ thay vì mặc định 12
} );
Điều này giảm bớt lỗi do nonce hết hạn — nhưng cũng tạo ra khoảng thời gian dài hơn để kẻ tấn công có thể lợi dụng một nonce bị đánh cắp. Chỉ dùng cách này khi các phương pháp khác không khả thi.
Kiểm tra kết quả
Mở DevTools → tab Network, kích hoạt REST call và kiểm tra request:
- Status phản hồi là
200, không phải403 - Request headers có
X-WP-Nonce: <hash> - Request headers có
Cookie: wordpress_logged_in_...— xác nhận auth cookie đã được gửi thành công
Kiểm tra nhanh trên console để xác nhận mọi thứ hoạt động từ đầu đến cuối:
fetch( wpApiSettings.root + 'wp/v2/users/me', {
credentials: 'include',
headers: { 'X-WP-Nonce': wpApiSettings.nonce },
} )
.then( r => r.json() )
.then( u => console.log( 'Đang đăng nhập với tên:', u.name ) );
// Phải in ra tên đăng nhập của bạn, không phải lỗi 403
Lưu ý thêm
Xác thực bằng cookie + nonce chỉ hoạt động trong cùng origin. Phương thức này hoạt động khi trình duyệt và WordPress dùng chung một domain và người dùng đang có phiên đăng nhập. Các frontend cross-origin và các cuộc gọi server-to-server cần một phương thức khác — hãy dùng Application Passwords, tích hợp sẵn trong WordPress từ phiên bản 5.6. Vào Users → Profile → Application Passwords, tạo một mật khẩu cho mỗi integration, và xác thực bằng HTTP Basic:
curl -u "your_username:xxxx xxxx xxxx xxxx xxxx xxxx" \
https://your-site.com/wp-json/wp/v2/posts
Mỗi integration dùng một mật khẩu riêng — thu hồi ngay khi integration đó ngừng hoạt động. Để tạo thông tin xác thực ngẫu nhiên mạnh cho mục đích kiểm thử hoặc tài khoản dịch vụ, toolcraft.app/en/tools/security/password-generator tạo mật khẩu ngay trên trình duyệt mà không gửi bất kỳ dữ liệu nào lên server.

