Cách kiểm tra API endpoint: DNS, TLS, header và CORS

Điểm chính

"API bị sập" hầu như không bao giờ là một lỗi đơn lẻ. Hãy kiểm tra qua sáu lớp (DNS, cổng 443, TLS, trạng thái HTTP, CORS, payload) và dừng ở lớp đầu tiên bị lỗi. Thủ phạm thường gặp là bản ghi DNS chưa kịp lan truyền, chứng chỉ hết hạn qua đêm, hoặc thiếu header Access-Control-Allow-Origin khiến lệnh gọi từ trình duyệt thất bại trong khi curl vẫn chạy bình thường.

Kiểm tra ngay

Sơ đồ sáu lớp cần kiểm tra khi test một API endpoint công khai: phân giải DNS, cổng 443, chứng chỉ TLS, header HTTP, CORS và payload JSON
Sơ đồ sáu lớp cần kiểm tra khi test một API endpoint công khai: phân giải DNS, cổng 443, chứng chỉ TLS, header HTTP, CORS và payload JSON
Bắt đầu đọc
  1. Vì sao "API bị sập" không bao giờ là một vấn đề đơn lẻ
  2. Chọn một endpoint thật để thoải mái thử nghiệm
  3. Lớp 1: tên miền có phân giải được không?
  4. Lớp 2: cổng 443 có mở không?
  5. Lớp 3: chứng chỉ TLS có hợp lệ không?
  6. Lớp 4: đọc các header phản hồi
  7. Một phản hồi JSON khỏe mạnh trông như thế nào
  8. Lớp 5: header chỉ làm hỏng lệnh gọi từ trình duyệt
  9. Cái bẫy preflight: chạy tốt cho đến khi bạn thêm một header
  10. Lớp 6: payload có đúng cấu trúc bạn mong đợi không?
  11. Triệu chứng → lớp → công cụ
  12. Câu hỏi thường gặp

Vì sao "API bị sập" không bao giờ là một vấn đề đơn lẻ

Khi một lệnh gọi API thất bại, lỗi mà client hiển thị thường được sinh ra ở nhiều lớp phía trên so với chỗ thực sự hỏng. Trình duyệt báo Failed to fetch không thể cho bạn biết tên miền không phân giải được, kết nối TCP bị từ chối, bắt tay TLS bị loại, hay máy chủ đã trả về một mã 200 hoàn toàn bình thường nhưng trình duyệt vứt đi vì thiếu header CORS. Cả bốn trường hợp đều cho cùng một thông báo.

Cách xử lý là đừng đoán mò nữa mà đi qua từng lớp theo thứ tự. Mỗi lớp chỉ có ý nghĩa nếu lớp bên dưới đã thành công, nên lớp đầu tiên bị lỗi chính là lỗi thật của bạn — mọi thứ sau đó chỉ là nhiễu.

Có sáu lớp đáng kiểm tra, và tất cả mất khoảng hai phút:

  • DNS — tên miền có phân giải ra địa chỉ IP không?
  • Cổng 443 — có gì đang lắng nghe không, hay kết nối bị từ chối hoặc bị lọc?
  • TLS — chứng chỉ có hợp lệ, còn hạn, và được gửi kèm chuỗi đầy đủ không?
  • Trạng thái HTTP — máy chủ thực sự trả về gì: 200, 401, 429, 503?
  • CORS — phản hồi có chứa các header mà trình duyệt cần để giao phần nội dung cho bạn không?
  • Payload — JSON có đúng cấu trúc mà code của bạn mong đợi không?

Kiểm tra trạng thái và header của endpoint ngay

Chọn một endpoint thật để thoải mái thử nghiệm

Để thực hành, bạn cần một endpoint đang hoạt động, trả về dữ liệu thật, không cần khóa API và không giới hạn tốc độ khi bạn chạy cùng một yêu cầu hai mươi lần. Điều đó loại bỏ hầu hết các ứng viên hiển nhiên — gần như mọi API công khai phổ biến hiện nay đều bắt bạn qua biểu mẫu đăng ký và token, nghĩa là một yêu cầu thất bại luôn có thể do thông tin xác thực của bạn chứ không phải lớp đang kiểm tra.

Các ví dụ dưới đây dùng API giờ cầu nguyện miễn phí của Waqt Azan (مواقيت الصلاة), một dịch vụ giờ cầu nguyện tiếng Ả Rập phủ hơn 60.000 địa điểm tại hơn 100 quốc gia. Nó phù hợp làm endpoint mẫu vì ba lý do: không có khóa hay header xác thực, trả về JSON có cấu trúc thay vì HTML, và đặt chính sách CORS thoáng — nên các lệnh curl chạy đúng như viết, còn ví dụ fetch() chạy được trong console của một trang không có Content-Security-Policy nghiêm ngặt.

Endpoint nhận slug quốc gia và thành phố ngay trong đường dẫn:

bash
# Prayer times for a city — no API key, no headers required
curl -s https://waqtazan.com/api/v1/timings/saudi-arabia/riyadh

# The same data as a subscribable calendar feed
curl -s https://waqtazan.com/api/v1/ical/saudi-arabia/riyadh.ics

Hãy thay bằng bất kỳ host nào bạn đang gỡ lỗi — sáu lớp này giống hệt nhau dù bạn đang kiểm tra API công khai, máy chủ staging của riêng mình hay bộ nhận webhook của bên thứ ba.

Lớp 1: tên miền có phân giải được không?

DNS là thủ phạm thường gặp. Một bản ghi vừa được đổi và chưa lan truyền, một wildcard đã bị xóa, hoặc API nằm trên một subdomain chưa từng được tạo.

Hãy phân giải tên miền trước mọi thứ khác. Nếu bước này không trả về gì thì khởi động lại ứng dụng bao nhiêu lần cũng vô ích — client của bạn thậm chí không đến được bước mở socket.

bash
dig +short waqtazan.com A
nslookup waqtazan.com

# Compare what different resolvers see — catches mid-propagation records
dig +short waqtazan.com A @1.1.1.1
dig +short waqtazan.com A @8.8.8.8

Nếu hai resolver công khai cho kết quả khác nhau ngay sau khi bạn đổi bản ghi, nghĩa là nó vẫn đang lan truyền, và API sẽ có vẻ chập chờn tùy mỗi client rơi vào resolver nào (CDN định tuyến theo địa lý có thể luôn trả mỗi resolver một địa chỉ khác nhau, nên hãy so với bản ghi bạn đã đặt). Hãy chạy kiểm tra DNS để xem đầy đủ các bản ghi, hoặc kiểm tra sự lan truyền trên các resolver toàn cầu bằng công cụ kiểm tra lan truyền DNS ngay sau khi đổi bản ghi.

Kết quả trống ở đây tương ứng trực tiếp với ERR_NAME_NOT_RESOLVED trong Chrome và getaddrinfo ENOTFOUND trong Node — bài ERR_NAME_NOT_RESOLVED có danh sách đầy đủ cách khắc phục.

Một điều không cần lo: nhiều API nằm sau CDN, nên bản ghi A trỏ tới điểm biên của CDN chứ không phải máy chủ gốc. Điều đó là bình thường. Quan trọng là bạn nhận được câu trả lời, không phải IP nào.

Lớp 2: cổng 443 có mở không?

Tên miền đã phân giải, nên có thứ gì đó mang địa chỉ này. Giờ hãy xem có gì đang lắng nghe trên đó không. Đây là nơi bạn phân biệt ba lỗi rất khác nhau mà client thường báo giống hệt nhau:

  • Mở (open) — bắt tay TCP hoàn tất. Chuyển sang lớp tiếp theo.
  • Bị từ chối (refused) — máy chủ trả lời ngay bằng RST. Có thứ gì đó ở đó, nhưng không có tiến trình nào gắn với cổng này — thường là dịch vụ bị sập, hoặc chỉ bind vào localhost.
  • Bị lọc (filtered) — kết nối treo cho đến khi hết thời gian chờ. Tường lửa hoặc security group đang lặng lẽ bỏ gói tin, vì vậy yêu cầu của bạn treo một phút hoặc hơn trước khi thất bại (75 giây trên macOS).
bash
# Time-boxed TCP check: -w limits the connect on Linux; on macOS use -G 5 instead
nc -vz -w 5 waqtazan.com 443

# Or with curl: --connect-timeout caps the connect; 000 means no response came back
curl -s -o /dev/null -w '%{http_code}\n' --connect-timeout 5 https://waqtazan.com/

Kết nối bị từ chối và bị lọc cần cách sửa hoàn toàn khác nhau, nên đáng để biết bạn đang gặp loại nào. Công cụ kiểm tra cổng phân biệt được chúng và kiểm tra từ bên ngoài mạng của bạn — điều này quan trọng, vì một cổng mở khi dùng VPN văn phòng có thể bị tường lửa chặn với internet công khai.

Nếu curl in ra 000 làm mã trạng thái, nghĩa là không có phản hồi HTTP nào quay về. Thường yêu cầu đã thất bại ở các lớp 1–3 (không có câu trả lời DNS, cổng bị từ chối hoặc bị lọc, bắt tay TLS thất bại), nhưng máy chủ nhận kết nối rồi đóng nó hoặc không bao giờ trả lời cũng cho 000; hãy thêm -v để xem bước cuối cùng còn chạy được. ERR_CONNECTION_REFUSED trình bày chi tiết trường hợp bị từ chối.

Lớp 3: chứng chỉ TLS có hợp lệ không?

Cổng 443 đã mở, nhưng cổng mở không có nghĩa là endpoint HTTPS hoạt động. Chứng chỉ là nguyên nhân kinh điển gây hỏng qua đêm, vì chúng hết hạn theo lịch và tự động gia hạn hỏng một cách lặng lẽ.

Chứng chỉ cần đồng thời đúng ba điều: chưa hết hạn, tên khớp với hostname bạn yêu cầu, và máy chủ gửi đủ chuỗi chứng chỉ trung gian. Điều thứ ba là điều khó chịu nhất: chuỗi không đầy đủ thường vẫn chạy được trên trình duyệt, vì Chrome tự tải chứng chỉ trung gian còn thiếu và Firefox nạp sẵn chúng, trong khi Node, Python, Java và các bản curl dựng bằng OpenSSL thì thất bại (curl đi kèm macOS dùng bộ xác minh của Apple và có thể vẫn qua). Đó là nguồn gốc của báo lỗi kinh điển "chạy được trên trình duyệt của tôi nhưng không chạy trên production".

bash
# Expiry dates and subject
echo | openssl s_client -connect waqtazan.com:443 -servername waqtazan.com 2>/dev/null \
  | openssl x509 -noout -subject -dates

# Full chain as presented by the server
openssl s_client -connect waqtazan.com:443 -servername waqtazan.com -showcerts < /dev/null

Hãy đếm số chứng chỉ trong kết quả của -showcerts. Chỉ một chứng chỉ đơn lẻ thường nghĩa là thiếu chứng chỉ trung gian. Công cụ kiểm tra chứng chỉ SSL báo ngày hết hạn, độ đầy đủ của chuỗi và khớp hostname trong một lần chạy, còn bài chuỗi chứng chỉ SSL là gì giải thích vì sao chứng chỉ trung gian quan trọng đến vậy.

Hãy giữ -servername: OpenSSL 1.1.1 trở lên tự điền SNI từ hostname trong -connect, nhưng openssl LibreSSL đi kèm macOS thì không, và không phiên bản nào làm vậy khi bạn kết nối tới một địa chỉ IP. Không có SNI, máy chủ dùng chung sẽ đưa chứng chỉ mặc định của nó, hoặc như Cloudflare, ngắt bắt tay, và bạn sẽ đuổi theo một lỗi sai tên không hề tồn tại.

Lớp 4: đọc các header phản hồi

TLS ổn rồi, vậy là cuối cùng bạn có một cuộc hội thoại HTTP thực sự. Giờ hãy đọc những gì máy chủ thực sự nói, thay vì những gì client của bạn tóm tắt lại. Chỉ in header: phần nội dung vẫn được tải về rồi bỏ đi, nên đây chính là yêu cầu GET mà client của bạn gửi.

bash
curl -s -D - -o /dev/null https://waqtazan.com/api/v1/timings/egypt/cairo

Hãy đọc dòng trạng thái trước, rồi content-type, rồi mới đến phần còn lại. Hai trường đó cho bạn biết yêu cầu có thành công không và nội dung có phải JSON không, trước khi đọc bất kỳ thứ gì khác.

Một phản hồi JSON khỏe mạnh trông như thế nào

Chạy lệnh ở trên với endpoint thử nghiệm vào ngày 27 tháng 10 năm 2026 trả về các header sau, cùng một số header khác:

HeaderGiá trịĐiều nó cho bạn biết
HTTP/2200Yêu cầu thành công, và máy chủ dùng HTTP/2
content-typeapplication/jsonBạn sẽ nhận JSON, không phải trang lỗi HTML
content-length567Có payload thật, không phải nội dung rỗng
access-control-allow-origin*Trình duyệt được phép đọc phản hồi này
servercloudflareCó một CDN đứng trước máy chủ gốc
alt-svch3=":443"HTTP/3 được quảng bá cho các yêu cầu tiếp theo

Lớp 5: header chỉ làm hỏng lệnh gọi từ trình duyệt

Đây là lớp khiến bạn mất hàng giờ, vì đó là nơi curl và trình duyệt thực sự bất đồng. CORS do trình duyệt thực thi, không phải máy chủ. Nếu thiếu Access-Control-Allow-Origin, máy chủ vẫn trả về một mã 200 đầy đủ, đúng đắn — chỉ là trình duyệt từ chối giao phần nội dung cho JavaScript của bạn.

Vì vậy triệu chứng là: curl chạy hoàn hảo, Postman chạy hoàn hảo, còn console trình duyệt báo yêu cầu bị chặn bởi chính sách CORS. Máy chủ không có gì sai. Phản hồi chỉ thiếu một header.

Endpoint thử nghiệm trả về access-control-allow-origin: *, đó là lý do một fetch() thông thường từ bất kỳ origin nào cũng chạy được:

javascript
// Run it in the console of a tab on a site without a strict CSP, such as example.com
// (this page's own CSP blocks it). It works there because ACAO is set to *
const r = await fetch('https://waqtazan.com/api/v1/timings/egypt/cairo')
console.log(await r.json())

Cái bẫy preflight: chạy tốt cho đến khi bạn thêm một header

Còn một cái bẫy cấp hai nữa, và endpoint này minh họa nó rất rõ. Một yêu cầu đơn giản (GET thông thường không có header tùy chỉnh) thành công. Nhưng ngay khi bạn thêm một header tùy chỉnh, hoặc POST với Content-Type: application/json, trình duyệt sẽ gửi trước một yêu cầu preflight OPTIONS, và phản hồi preflight phải tự mang các header CORS (MDN).

Hãy kiểm tra preflight riêng, vì đó là một phản hồi khác từ một luồng xử lý khác:

bash
curl -s -D - -o /dev/null -X OPTIONS \
  -H 'Origin: https://example.com' \
  -H 'Access-Control-Request-Method: GET' \
  https://waqtazan.com/api/v1/timings/egypt/cairo

Tính đến ngày 27 tháng 10 năm 2026, preflight đó trả về mã 200 với content-type: text/html và hoàn toàn không có header access-control-allow-origin, nên một yêu cầu có preflight từ trình duyệt sẽ bị chặn, dù GET thông thường vẫn chạy tốt.

Đó chính là cái bẫy: tích hợp của bạn chạy tốt khi phát triển, bạn thêm header Authorization hoặc X-Request-Id, và nó hỏng mà máy chủ không hề thay đổi gì. Nếu bạn chỉ cần dữ liệu công khai, hãy giữ yêu cầu ở dạng đơn giản và nó sẽ tiếp tục chạy.

Lớp 6: payload có đúng cấu trúc bạn mong đợi không?

Lớp cuối cùng là lớp mà code của bạn thực sự quan tâm. Một mã 200 với JSON hợp lệ vẫn có thể làm bạn hỏng việc nếu một trường bị đổi tên, kiểu dữ liệu chuyển từ số sang chuỗi, hoặc dấu thời gian đến theo định dạng bạn không phân tích được.

Hãy xem phản hồi thật trước khi viết parser. Endpoint này trả về cả giờ đã định dạng để hiển thị lẫn dấu thời gian ISO, một mẫu thiết kế đáng bắt chước — chuỗi đã bản địa hóa dành cho con người, giá trị ISO dành cho code:

json
{
  "city": { "slug": "riyadh", "name_ar": "الرياض", "name_en": "Riyadh" },
  "country": "saudi-arabia",
  "date": "2026-10-26",
  "times":    { "fajr": "4:38 ص", "dhuhr": "11:37 ص", "maghrib": "5:17 م" },
  "timesISO": { "fajr": "2026-10-26T01:38:00.000Z", "dhuhr": "2026-10-26T08:37:00.000Z" },
  "method": "umm_al_qura"
}

Luôn phân tích trường đọc được bằng máy. Ở đây đó là timesISO, không bao giờ là times — các chuỗi hiển thị được bản địa hóa và sẽ đổi định dạng ngay khi ngôn ngữ đổi.

Hãy chuyển phản hồi qua jq để xem nhanh cấu trúc: curl -s <url> | jq keys liệt kê các trường cấp cao nhất, còn jq '.times' đi sâu vào mà không phải cuộn qua nội dung thô.

Triệu chứng → lớp → công cụ

Hãy dùng bảng này để nhảy thẳng tới lớp cần xem, thay vì đi qua cả sáu lớp mỗi lần:

Bạn thấy gìLớp bị lỗiKiểm tra bằng
ERR_NAME_NOT_RESOLVED, ENOTFOUND1 — DNSKiểm tra DNS
ECONNREFUSED, thất bại ngay lập tức2 — CổngKiểm tra cổng
Yêu cầu treo rồi hết thời gian chờ2 — Cổng bị lọcKiểm tra cổng
CERT_HAS_EXPIRED, UNABLE_TO_VERIFY_LEAF_SIGNATURE3 — TLSKiểm tra SSL
Chạy trên trình duyệt, lỗi trên curl hoặc Node3 — Chuỗi chứng chỉ không đầy đủKiểm tra SSL
401, 429, 5034 — Trạng thái HTTPKiểm tra HTTP header
Chạy trên curl, bị chặn trên trình duyệt5 — CORSKiểm tra HTTP header
Unexpected token '<', "<!DOCTYPE "... is not valid JSON4 — Trang lỗi HTMLKiểm tra HTTP header
Thiếu trường hoặc sai kiểu6 — Payloadcurl … | jq

Độ trễ nằm hơi ngoài bậc thang này. Nếu mọi lớp đều qua nhưng phản hồi chậm, hãy đo thời gian khứ hồi tới host bằng ping và kiểm tra xem chính việc phân giải tên có phải nút thắt không bằng kiểm tra tốc độ DNS.

Kiểm tra header phản hồi thật của mọi endpoint

Xem chính xác mã trạng thái, content-type và các header CORS mà API trả về. Yêu cầu được gửi từ phía máy chủ, nên bộ nhớ đệm trình duyệt hay tiện ích mở rộng không thể làm sai lệch kết quả. Miễn phí, không cần đăng ký.

Nguồn

  1. Cross-Origin Resource Sharing (CORS), MDN
  2. Fetch Standard: CORS-safelisted request headers, WHATWG

Câu hỏi thường gặp

Làm sao kiểm tra API endpoint mà không dùng Postman?

curl đáp ứng mọi thứ Postman làm khi kiểm tra cơ bản. Dùng curl -s -D - -o /dev/null <url> để chỉ xem header phản hồi, curl -s <url> | jq để xem nội dung JSON, và curl -s -o /dev/null -w '%{http_code}' để chỉ in mã trạng thái. Để kiểm tra nhanh mà không cần terminal, một công cụ kiểm tra HTTP header trực tuyến trả về cùng thông tin ngay trên trình duyệt.

Vì sao API chạy trên curl nhưng lỗi trên trình duyệt?

Gần như luôn là CORS. Máy chủ trả về phản hồi hợp lệ trong cả hai trường hợp, nhưng trình duyệt từ chối đưa nội dung cho JavaScript trừ khi phản hồi có header Access-Control-Allow-Origin cho phép origin của bạn. curl bỏ qua hoàn toàn CORS, vì CORS là chính sách bảo mật của trình duyệt chứ không phải của máy chủ. Nếu thiếu header, máy chủ phải gửi nó — không có cách sửa nào ở phía client.

Curl trả về mã trạng thái 000 nghĩa là gì?

Nghĩa là không nhận được phản hồi HTTP nào, nên không có mã trạng thái để báo. Thường lỗi xảy ra trước HTTP: DNS không phân giải, kết nối TCP bị từ chối hoặc bị lọc, hoặc bắt tay TLS thất bại. Máy chủ nhận kết nối rồi đóng nó hoặc không bao giờ trả lời cũng cho 000, nên hãy chạy lại lệnh với -v để xem bước cuối cùng còn chạy được.

Làm sao biết API bị sập với mọi người hay chỉ với riêng tôi?

Hãy kiểm tra từ bên ngoài mạng của bạn. Công cụ kiểm tra cổng và DNS chạy phía máy chủ loại bỏ resolver cục bộ, VPN, tường lửa công ty và bộ nhớ đệm trình duyệt khỏi phương trình. Nếu endpoint trả lời công cụ bên ngoài nhưng không trả lời máy của bạn, vấn đề nằm ở phía bạn — thường là bộ nhớ đệm DNS hoặc một quy tắc tường lửa.

Vì sao API trả về 200 nhưng trình phân tích JSON của tôi báo lỗi?

Gần như chắc chắn bạn đang nhận HTML với mã 200. Captive portal, proxy và một số trang lỗi trả về mã thành công kèm nội dung HTML. Hãy xem header content-type: nếu nó là text/html thay vì application/json thì nội dung là một trang web, và lỗi phân tích chỉ là triệu chứng chứ không phải nguyên nhân.

CORS preflight là gì và khi nào xảy ra?

Preflight là một yêu cầu OPTIONS tự động mà trình duyệt gửi trước yêu cầu thật mỗi khi yêu cầu không phải loại 'đơn giản': dùng phương thức khác GET, HEAD hoặc POST, đặt header tùy chỉnh như Authorization, hoặc gửi Content-Type khác dữ liệu biểu mẫu hay văn bản thuần (application/json thuộc loại này). Bản thân phản hồi preflight phải có các header CORS. Đó là lý do một tích hợp có thể chạy tốt cho đến khi bạn thêm header xác thực, rồi hỏng mà máy chủ không đổi gì.

Nên kiểm tra DNS của API bằng resolver của mình hay resolver công khai?

Cả hai, rồi so sánh. Resolver của nhà mạng có thể trả về bản ghi cũ trong bộ nhớ đệm rất lâu sau khi bản ghi gốc đã đổi. Truy vấn 1.1.1.1 và 8.8.8.8 cùng với resolver mặc định cho bạn biết bạn đang thấy độ trễ lan truyền hay một bản ghi thực sự bị thiếu.

Cổng 443 mở có nghĩa là endpoint HTTPS hoạt động không?

Không. Cổng mở chỉ xác nhận có thứ gì đó đã chấp nhận kết nối TCP. Bắt tay TLS vẫn có thể thất bại vì chứng chỉ hết hạn, hostname không khớp, hoặc chuỗi chứng chỉ trung gian không đầy đủ. Hãy kiểm tra chứng chỉ riêng sau khi xác nhận cổng đã mở.

Về tác giả

Shaik Vahid

Nhà sáng lập DNS Robot. Vahid đã làm nhiều năm trong bộ phận hỗ trợ hosting, xử lý tên miền, bản ghi DNS, chứng chỉ SSL và việc gửi email cho khách hàng. Anh xây dựng các công cụ của DNS Robot và viết những hướng dẫn này từ các lỗi mà mọi người thực sự gặp.

Kiểm tra HTTP Header

Phân tích header phản hồi HTTP và chấm điểm header bảo mật