Ứng dụng ngoài gọi WordPress REST API thường lỗi ở năm chỗ: đường dẫn không có route, rewrite hoặc permalink không chuyển yêu cầu đúng, tài khoản thiếu quyền, header xác thực không tới được WordPress, và một lớp đứng trước WordPress trả về trang không phải JSON. Mỗi mã lỗi trong phản hồi mang một nghĩa riêng. Đọc mã trước khi sửa code tiết kiệm được nhiều giờ dò tìm. Bài này đi qua bảy nhóm lỗi hay gặp, nêu mã và thông điệp mà WordPress trả về, rồi đưa thứ tự gỡ lỗi chung. Các mã và thông điệp dưới đây lấy từ tài liệu chính thức của WordPress hoặc từ phản hồi thật khi gọi site công khai developer.wordpress.org ngày 13/10/2026. Mã nào chưa tái tạo được sẽ được ghi rõ.
Nếu chưa rõ WordPress làm gì với nội dung và vì sao REST API tồn tại, đọc trước WordPress là gì và khi nào đủ dùng sẽ tiện hơn. Nếu site đang chạy theo kiểu headless, headless WordPress khác WordPress thường ở điểm nào là bối cảnh gần nhất. Phần lớn bài này giả định site đã chạy và ứng dụng cần đọc hoặc ghi dữ liệu qua REST.
Bảng tra nhanh mã lỗi và việc làm đầu tiên
Bảng dưới liệt kê các mã hay gặp. Cột cuối là việc cần làm trước khi đoán nguyên nhân, vì mỗi việc chỉ mất vài giây và loại được một nhóm nguyên nhân.
| Mã lỗi (HTTP) | Nguyên nhân thường gặp | Việc làm đầu tiên |
|---|---|---|
| 404 rest_no_route | Route không tồn tại: sai namespace, sai rest_base, hoặc custom post type chưa bật show_in_rest | Gọi GET /wp/v2/types và đối chiếu rest_base với endpoint đang gọi |
| 404 nhưng thân là HTML | Host không chạy WordPress, hoặc CDN, proxy trả trang lỗi thay cho WordPress | Xem Content-Type và thử cùng đường dẫn dạng ?rest_route= |
| 401 rest_not_logged_in | Endpoint cần đăng nhập, hoặc header xác thực không tới được WordPress | Gọi cùng endpoint có và không có header Authorization |
| 401 rest_forbidden_context | Gọi với context=edit khi chưa đăng nhập | Bỏ context=edit, hoặc dùng tài khoản có quyền biên tập |
| 401 rest_cannot_create | Gửi POST tạo nội dung khi chưa xác thực | Kiểm tài khoản và header Authorization trong yêu cầu thực tế |
| 400 rest_invalid_param, rest_out_of_bounds | per_page ngoài khoảng 1 đến 100, page nhỏ hơn 1 | Đọc khối params trong JSON và sửa đúng tham số được nêu tên |
| 400 rest_invalid_param, chi tiết rest_forbidden_status | Yêu cầu status=draft hoặc status=private khi không có quyền | Xác thực bằng tài khoản có quyền đọc đúng trạng thái đó |
| Không có mã WordPress, chỉ có lỗi CORS trong console | Trình duyệt chặn đọc phản hồi vì thiếu header Access-Control-Allow-Origin | Mở tab Network, đọc status và header của đúng request |
Gọi REST API bị trả về 404 thì kiểm gì trước
Có hai loại 404 cần tách ra, vì cách sửa khác nhau. Loại thứ nhất là WordPress trả lời và báo không có route. Khi gọi một đường dẫn không tồn tại trên developer.wordpress.org, WordPress trả về HTTP 404 với code rest_no_route và thông điệp “No route was found matching the URL and request method.” Mã này cho biết yêu cầu đã tới WordPress nhưng không khớp route nào cả về đường dẫn lẫn phương thức. Lỗi thường nằm ở tên namespace, tên endpoint hoặc phương thức gọi, không nằm ở xác thực.
Loại thứ hai là 404 mà thân phản hồi không phải JSON. Khi gọi https://example.com/wp-json/wp/v2/posts, máy chủ trả về trang HTML “Example Domain” với HTTP 404. Site đó không chạy WordPress, nên không có lỗi REST nào được sinh ra. Dấu hiệu nhận biết là Content-Type không phải application/json và thân không có trường code kiểu rest_*.
Theo tài liệu REST API của WordPress, đường dẫn gốc có hai dạng: /wp-json/ và /?rest_route=. Khi permalink là Plain, hoặc khi rewrite chưa được ghi, dạng /wp-json/ có thể không chạy, và cách thử nhanh là dạng ?rest_route=. Tài liệu FAQ nêu một nguyên nhân cụ thể trên Nginx: nếu try_files dùng $args thay vì $is_args$args, tham số truy vấn không được chuyển đúng và ?rest_route= hỏng. Cách sửa là đổi sang $is_args$args, kiểm cú pháp bằng nginx -t rồi mới reload.
Có một bẫy cần lưu ý. Trên site không phải WordPress, ?rest_route= vẫn trả về HTTP 200 kèm trang HTML. Ví dụ https://example.com/?rest_route=/wp/v2/posts trả về 200 và nội dung HTML của trang chủ. Mã 200 không chứng minh REST đang chạy, nên việc kiểm đúng là xem thân phản hồi có phải JSON hay không.
curl -i "https://example.com/wp-json/wp/v2/posts?per_page=1&_fields=id"
curl -i "https://example.com/?rest_route=/wp/v2/posts&per_page=1&_fields=id"
Nếu dạng /wp-json/ lỗi mà dạng ?rest_route= trả JSON, lỗi nằm ở rewrite, tức permalink hoặc cấu hình máy chủ. Nếu cả hai đều lỗi, kiểm tên route bằng curl -i "https://example.com/wp-json/wp/v2/types". Endpoint này liệt kê các loại nội dung mà site đang đưa ra qua REST, kèm rest_base của từng loại. Trên developer.wordpress.org, loại post có rest_base là posts, khớp với đường dẫn /wp/v2/posts.
Lỗi 401 khi xác thực khác gì với 403
Khi gọi endpoint cần đăng nhập mà không gửi thông tin xác thực, WordPress trả HTTP 401 với code rest_not_logged_in và thông điệp “You are not currently logged in.”. Trên developer.wordpress.org, endpoint /wp/v2/users/me trả đúng kết quả này khi gọi ẩn danh.
Tài liệu xác thực nêu ba cơ chế. Cơ chế mặc định là cookie kèm nonce, nonce gửi qua header X-WP-Nonce, và cookie chỉ hoạt động khi API được gọi từ bên trong WordPress bởi người dùng đã đăng nhập. Cơ chế thứ hai là Application Passwords, có từ WordPress 5.6, dùng Basic Auth và được tạo trong trang sửa người dùng. Cơ chế thứ ba là các plugin bổ sung như OAuth 1.0a, JSON Web Token hoặc Basic Authentication. Với ứng dụng ngoài, cookie không dùng được, nên đường đi thường gặp là Application Passwords qua HTTPS. Tài liệu nói rõ thông tin Basic Auth được gửi qua HTTPS, và plugin Basic Authentication chỉ phù hợp để phát triển hoặc thử nghiệm.
Khi header Authorization không tới được WordPress, tài liệu FAQ nêu hai nguyên nhân ở máy chủ. Với Apache, cần thêm SetEnvIf Authorization. Với Nginx khi chạy qua PHP-FPM, cần fastcgi_pass_header Authorization. Cùng một request có thể thành công bằng curl mà thất bại từ ứng dụng, và khác biệt đó thường nằm ở việc header có được gửi đi hay không.
Về 403, phần này cần tách rõ. Trong các lần gọi ẩn danh hôm nay, mọi lỗi về quyền đều trả HTTP 401, kèm data.status bằng 401. Mã 403 kiểu rest_forbidden hay rest_cannot_access chưa tái tạo được trên site công khai, vì để thấy 403 cần một tài khoản đã đăng nhập nhưng thiếu quyền. Tài liệu đã đọc cũng không nêu điều kiện trả 403. Khi gặp 403, cách kiểm là gọi lại đúng endpoint bằng hai tài khoản, một có quyền và một không có quyền, rồi đối chiếu vai trò của từng tài khoản trong WordPress.
Một cấu hình hay gặp làm 401 xuất hiện trên cả endpoint công khai. Theo phần “Require Authentication for All Requests” của FAQ, site có thể thêm kiểm tra is_user_logged_in vào filter rest_authentication_errors để yêu cầu xác thực cho mọi request. Đây là cách chặn truy cập ẩn danh có chủ ý, nên khi thấy ứng dụng không đọc được bài công khai, cần hỏi xem site có bật kiểu này không trước khi sửa code.

Bài riêng tư và bản nháp không hiện thì kiểm trạng thái và quyền
Bài ở trạng thái draft hoặc private không xuất hiện trong kết quả khi gọi ẩn danh. Khi gọi với status=draft hoặc status=private mà không xác thực, WordPress trả HTTP 400 với code rest_invalid_param, thông điệp “Invalid parameter(s): status” và chi tiết code rest_forbidden_status, nội dung “Status is forbidden.”. Điều quan trọng là mã này nói tham số bị từ chối, không nói bài không tồn tại. Nhiều đội nhầm lẫn lỗi này với việc bài đã bị xoá, rồi đi sửa phần sai.
Để đọc bản nháp hoặc bài riêng tư, cần xác thực bằng tài khoản có quyền đọc đúng trạng thái đó. Nếu cần trường đầy đủ, gọi thêm context=edit, nhưng context=edit yêu cầu quyền biên tập. Gọi context=edit khi ẩn danh trả HTTP 401 với code rest_forbidden_context và thông điệp “Sorry, you are not allowed to edit posts in this post type.”. Trước khi sửa code, kiểm vai trò của tài khoản đang dùng trong màn hình người dùng của WordPress. Tài khoản thiếu quyền sẽ không đọc được bản nháp mà nó không được phép xem.
curl -i -H "Authorization: Basic ****" "https://example.com/wp-json/wp/v2/posts?status=draft&context=edit"
Với nội dung đã chuyển sang headless, đọc bài qua REST thường đi cùng việc giữ đường dẫn cũ cho người đọc và máy tìm kiếm. Nếu đang cân nhắc cấu trúc URL, nghiên cứu về chuyển website WordPress sang headless và giữ link SEO trình bày các rủi ro này trước khi đổi bất cứ thứ gì.
Trình duyệt báo lỗi CORS thì sửa ở đâu
Lỗi CORS là lỗi do trình duyệt báo, không phải mã của WordPress. Khi phản hồi thiếu header Access-Control-Allow-Origin phù hợp, trình duyệt không cho trang đọc nội dung và console hiện thông báo bị chặn bởi chính sách CORS. Điều dễ nhầm là mã HTTP thật của phản hồi có thể là 200, 401 hay 500, và chính mã đó quyết định lỗi nằm ở đâu. Việc đầu tiên là mở tab Network, chọn đúng request và đọc cả status lẫn header của phản hồi.
Tài liệu FAQ ghi rằng REST API không kiểm header Origin của yêu cầu đến, nên endpoint công khai có thể bị gọi từ bất kỳ site nào. Trên developer.wordpress.org, yêu cầu OPTIONS với Origin https://example.com nhận lại header Access-Control-Allow-Origin đúng bằng giá trị đó. Vì vậy khi header thiếu, cần xem chính phản hồi để biết lớp nào đã trả lời, thay vì cho rằng WordPress không hỗ trợ CORS.
curl -i -X OPTIONS -H "Origin: https://app.example.net" -H "Access-Control-Request-Method: GET" "https://example.com/wp-json/wp/v2/posts"
Cách sửa đúng là không đặt khoá xác thực trong mã chạy ở trình duyệt, và không nới CORS cho mọi nguồn trên WordPress. Khoá đặt trong JavaScript ai mở trang cũng đọc được. Nới CORS rộng chỉ làm tăng bề mặt tiếp xúc, trong khi FAQ đã nói REST không xác minh Origin, nên bảo vệ phải đến từ xác thực và phân quyền. Cách đúng là để trình duyệt gọi máy chủ của chính ứng dụng, máy chủ này giữ Application Password và gọi WordPress. Mô hình đó cũng là lý do API ngân hàng cho phần mềm bán hàng được đưa qua máy chủ trung gian thay vì đi thẳng từ trình duyệt.
// Trình duyệt chỉ gọi máy chủ của ứng dụng
fetch("/api/bai-viet?per_page=10")
// Máy chủ của ứng dụng gọi WordPress, khoá nằm trong biến môi trường
// GET https://example.com/wp-json/wp/v2/posts?per_page=10
// Authorization: Basic ****
Tham số per_page quá 100 bị từ chối chứ không bị cắt
Tài liệu phân trang ghi per_page là số nguyên từ 1 đến 100, và per_page được giới hạn ở 100 bản ghi mỗi yêu cầu. Muốn lấy nhiều hơn, cần gọi nhiều trang bằng tham số page rồi tự gộp kết quả trong ứng dụng. Tài liệu cũng cho biết API mặc định chỉ trả về một số bản ghi giới hạn mỗi yêu cầu, nhưng không ghi con số mặc định cụ thể.

Tài liệu không nói rõ điều gì xảy ra khi vượt giới hạn. Khi thử hôm nay trên developer.wordpress.org, per_page=101 không bị cắt xuống 100. WordPress trả HTTP 400 với code rest_invalid_param, và chi tiết rest_out_of_bounds ghi “per_page must be between 1 (inclusive) and 100 (inclusive).” Với per_page=abc, lỗi là rest_invalid_type với nội dung “per_page is not of type integer.” Với page=0, lỗi là rest_out_of_bounds với nội dung “page must be greater than or equal to 1.” Việc cần làm khi gặp 400 là đọc khối params hoặc details trong JSON để biết tham số nào sai, rồi sửa đúng tham số đó.
curl -i "https://example.com/wp-json/wp/v2/posts?per_page=101"
Tài liệu cũng ghi hai header X-WP-Total, cho biết tổng số bản ghi trong collection, và X-WP-TotalPages, cho biết tổng số trang. Ứng dụng đồng bộ dữ liệu đọc hai header này để biết khi nào dừng vòng lặp, thay vì chạy cho tới khi gặp trang rỗng. Cách này tránh gọi thừa, và khi số bản ghi thay đổi giữa chừng, nó cho thấy số liệu có lệch hay không.
Lớp đứng trước WordPress trả về trang không phải JSON
Có lúc yêu cầu không tới được WordPress. Một lớp đứng trước, như CDN, tường lửa ứng dụng web hay chính sách bảo mật do nhà cung cấp hosting bật, có thể trả trang HTML hoặc mã lỗi của riêng nó, không có code rest_*. Nguồn chính thức của WordPress không mô tả điều kiện chặn cho từng host hay plugin, và bài này không nêu tên lớp bảo mật nào của bên khác vì chưa có nguồn để xác nhận. Điều kiểm được là hình dạng phản hồi. Khi lời gọi đến từ cổng thanh toán đi ngược về site, webhook thanh toán là gì và cách tích hợp mô tả chiều gọi ngược đó, và cần kiểm cùng lớp này.
Phản hồi của WordPress có Content-Type application/json, và thân có trường code cùng message. Phản hồi là HTML, hoặc có header do CDN hay WAF thêm vào, là do lớp khác trả lời. Khi đó việc sửa nằm ở cấu hình host hoặc lớp bảo mật, không nằm trong code WordPress. Để biết ai đang chặn, gọi cùng đường dẫn từ máy chủ ứng dụng và từ một máy ngoài, rồi so hai phản hồi. Nếu hai kết quả khác nhau, nguyên nhân nằm ở đường đi của yêu cầu, không nằm ở endpoint.
Khi đã có kết quả đúng từ WordPress, dấu hiệu nhận biết là 401 với code rest_not_logged_in như dưới đây. Đây là phản hồi bình thường, và nó cho biết yêu cầu đã tới WordPress.
curl -i "https://example.com/wp-json/wp/v2/users/me"
# Phản hồi của WordPress: HTTP 401, Content-Type: application/json, code rest_not_logged_in

Đổi tên custom post type mà REST không còn thấy
Route REST của một loại nội dung lấy từ rest_base. Theo tài liệu tham chiếu register_post_type, tham số show_in_rest mặc định là false và phải đặt true thì loại nội dung mới có trong REST API, cũng như trong trình soạn thảo khối. rest_base mặc định bằng tên post type. Khi đổi tên post type, route đổi theo, và ứng dụng đang gọi tên cũ sẽ nhận rest_no_route.
Có hai tình huống hay gặp. Tình huống thứ nhất là custom post type được tạo nhưng quên đặt show_in_rest, nên không có route nào cho nó. Tình huống thứ hai là tên post type được đổi, còn ứng dụng giữ endpoint cũ. Cách kiểm nhanh là gọi /wp/v2/types. Nếu loại nội dung không có trong danh sách, hoặc rest_base khác với endpoint đang gọi, lỗi nằm ở đăng ký post type chứ không nằm ở ứng dụng. Muốn giữ endpoint cũ trong khi đổi tên, đặt rest_base cố định khi đăng ký thay vì để nó theo tên post type.
curl -i "https://example.com/wp-json/wp/v2/types"
Đổi cấu trúc nội dung cũng ảnh hưởng tới các đường dẫn mà công cụ tìm kiếm đã lập chỉ mục. Nếu việc đổi tên đi kèm đổi URL, cần kiểm chuyển hướng trước khi công bố, và đọc cách WooCommerce headless xử lý giỏ hàng và thanh toán nếu site có thương mại điện tử đi kèm.
Quy trình gỡ lỗi theo thứ tự khi nhiều lỗi cùng xuất hiện
Khi chưa rõ lỗi nằm ở đâu, làm theo năm bước dưới đây. Bước nào đã loại được nguyên nhân thì dừng ở bước đó và sửa, không cần đi tiếp. Thứ tự này đi từ lớp ngoài vào trong, vì lỗi ở lớp ngoài thường che mất lỗi ở lớp trong.
- Kiểm /wp-json/ hoặc ?rest_route= có trả về JSON hay không. Nếu không có JSON, lỗi nằm ở host, rewrite hoặc lớp đứng trước WordPress, và việc kiểm route chưa có ý nghĩa.
- Kiểm permalink và route. Gọi /wp/v2/types để xác nhận loại nội dung và rest_base, rồi gọi endpoint đúng tên, đúng phương thức.
- Kiểm quyền. Gọi cùng endpoint ẩn danh và bằng tài khoản thử. Nếu 401 rest_not_logged_in đổi thành dữ liệu khi có xác thực, lỗi nằm ở xác thực. Nếu vẫn 401 hay 403 khi đã đăng nhập, kiểm vai trò của tài khoản.
- Kiểm header. Đọc Authorization, Content-Type và X-WP-Nonce nếu có trong request thực tế của ứng dụng. Trên trình duyệt, đọc thêm Access-Control-Allow-Origin của phản hồi. Với máy chủ web, kiểm xem header Authorization có bị bỏ qua khi chuyển sang PHP hay không.
- Kiểm log máy chủ. Đọc access log và error log của Nginx hoặc Apache trong đúng khoảng thời gian gọi, cùng với log PHP của WordPress. Log cho biết yêu cầu có tới được PHP không và có lỗi PHP nào sinh ra không.

Hỏi đáp
Gọi REST API bằng JavaScript trên trình duyệt có được không?
Được với endpoint công khai, nhưng khi trình duyệt chặn bởi CORS, phần lớn ứng dụng chuyển lời gọi sang máy chủ của chính mình. Khoá xác thực không bao giờ đặt trong mã chạy trên trình duyệt, vì bất kỳ ai mở trang cũng đọc được mã đó. Với endpoint cần đăng nhập, đây là điều kiện tối thiểu.
401 và 403 khác nhau thế nào khi gọi WordPress?
Trong các lần thử hôm nay trên site công khai, mọi lỗi quyền gặp được đều là 401 vì gọi ẩn danh. Mã 403 chưa tái tạo được, vì cần một tài khoản đã đăng nhập nhưng thiếu quyền. Với 401, việc cần làm là gửi thông tin xác thực. Với 403, việc cần làm là đối chiếu vai trò của tài khoản trong WordPress với thao tác đang gọi.
Postman gọi được nhưng ứng dụng lại lỗi, lý do là gì?
Postman không chạy chính sách CORS của trình duyệt, nên một phản hồi bị trình duyệt chặn vẫn hiện đầy đủ trong Postman. Nguyên nhân thứ hai thường là header: ứng dụng không gửi Authorization, hoặc máy chủ web bỏ header này khi chuyển cho PHP. Tài liệu FAQ nêu cách sửa cho Apache và Nginx như đã nói ở trên. So sánh request thực tế từ ứng dụng với lệnh curl để thấy khác biệt về header.
Bản nháp có đọc được qua REST không?
Đọc được nếu yêu cầu được xác thực bằng tài khoản có quyền đọc trạng thái draft. Gọi ẩn danh sẽ nhận rest_invalid_param với chi tiết rest_forbidden_status. Sau khi xác thực mà vẫn không thấy bản nháp, kiểm lại trạng thái bài trong WordPress và vai trò của tài khoản đang dùng.
Đổi tên custom post type xong, endpoint cũ còn dùng được không?
Không, nếu rest_base vẫn mặc định theo tên post type. Tên mới tạo route mới, còn tên cũ trả rest_no_route. Để giữ endpoint cũ cho ứng dụng đang chạy, khai báo rest_base cố định khi đăng ký post type, rồi kiểm lại bằng /wp/v2/types.
Đối với site WordPress đã chạy làm nguồn nội dung cho một ứng dụng riêng, phần kết nối là nơi các lỗi trong bài này xuất hiện. MONA nhận phần kiểm và sửa lớp kết nối đó trong dịch vụ lập trình website headless WordPress, gồm kiểm và sửa đường đi của yêu cầu từ ứng dụng tới WordPress. Bài này chỉ liệt kê những gì kiểm được hôm nay, còn site cụ thể cần đọc phản hồi và log thật mới chẩn đoán được. Đội kỹ thuật muốn trao đổi trực tiếp về lỗi đang gặp có thể gọi hotline 1900 636 648.




















































