🪁

Main
CASE FILE // 1_HTTP-200-NOI-DUNG-NOT-FOUNDDEPARTMENT: DEBUG > DEBUGGING
FILED: 2026-07-26

HTTP 200, nội dung Not Found

Một case production debugging về GitHub token, Vercel deploy và raw.githubusercontent.com.


title: "HTTP 200, nội dung Not Found" subtitle: "Một triệu chứng, ba nguyên nhân" description: "Một case production debugging về GitHub token, Vercel deploy và raw.githubusercontent.com." dateCreated: "2026-07-26" dateUpdated: "2026-07-26"

kind: "article" topic: "debugging" category: "debug" experience: "detective" interactive: true difficulty: 3 estimatedMinutes: 12

status: "published" language: "vi"

tags:

  • nextjs
  • vercel
  • github-api
  • production-debugging
  • mdx

Hồ sơ vụ án

HTTP trả 200.

Trang vẫn nói Not Found.

Local chạy. Production trắng.

Nếu status code không báo lỗi, vậy ai đang nói dối?

Tình huống bắt đầu khá mâu thuẫn:

Trang blog load bình thường, shell layout hiển thị, nhưng phần nội dung MDX lại hiển thị màn hình 404 Not Found từ Next.js.

Bảng nghi phạm

  • Token hết hạn

    STATUS: possible
  • Vercel Deploy bị tắt

    STATUS: possible
  • Thiếu file MDX

    STATUS: eliminated
  • raw.githubusercontent.com

    STATUS: possible
  • Biến môi trường Production

    STATUS: possible
  1. Cập nhật bài viết mới

    Đẩy file mdx mới lên repo content. Reload trang production: Không thấy bài viết.

  2. Phát hiện lỗi

    Các bài viết cũ cũng đã biến thành Not Found.

Ghi chú điều tra

Nghi phạm đầu tiên xuất hiện rất sớm: Token. Mã thông báo cá nhân (PAT) của GitHub lưu trong biến môi trường đã hết hạn.

Hệ thống cũ gọi API với header sau:

Header cũ gửi lên GitHub

contradicts
function githubHeaders(): HeadersInit {
  const headers = {
    Accept: "application/vnd.github+json",
    "X-GitHub-Api-Version": "2022-11-28",
  };

  if (process.env.GITHUB_API_TOKEN) {
    headers.Authorization = `Bearer ${process.env.GITHUB_API_TOKEN}`;
  }

  return headers;
}

Token hết hạn làm request bị từ chối. Logic khi đó coi việc fetch lỗi đồng nghĩa với việc bài viết không tồn tại.

Quan sát

Không nên để public content phụ thuộc vào token. Token chỉ nên giúp hệ thống mạnh hơn, không được trở thành điểm khiến nó gãy.

Thế nhưng, sau khi xoá token ở local và thấy local chạy ổn, production vẫn tiếp tục hiện Not Found. Vì sao?

Cấu hình Vercel

supports
{
  "git": {
    "deploymentEnabled": false
  }
}

Deployment tự động từ Git của Vercel đã bị tắt. Pipeline chạy qua GitHub Actions và sự khác biệt giữa Node 18.20.2 và phiên bản Vercel CLI mới đã tạo ra "ảo giác" deploy thành công. Local đã xoá token, nhưng production thì vẫn còn giữ token hết hạn. Production không thiếu biến môi trường, production dư một biến đã hỏng.

Tại sao logic retry tự động không hoạt động?

Tại sao logic retry tự động không hoạt động?
Logic cũ kỳ vọng một lỗi Authentication (401/403) nhưng lại nhận được 404 Not Found từ CDN.

Test thử với curl có token hết hạn

supports
Terminal
$curl -o /dev/null -w "%" -H "Authorization: Bearer expired" https://raw.githubusercontent.com/user/repo/main/file.json
404

Test thử không có token

supports
Terminal
$curl -o /dev/null -w "%" https://raw.githubusercontent.com/user/repo/main/file.json
200

Vấn đề gốc nằm ở chỗ hệ thống gọi hai endpoint khác nhau:

  1. api.github.com/.../git/trees - trả về 401 khi token sai.
  2. raw.githubusercontent.com - trả về 404 khi token sai.
Before
if (
  (res.status === 401 || res.status === 403) &&
  process.env.GITHUB_API_TOKEN
) {
  return fetch(url, { headers: githubBaseHeaders() });
}
After
async function githubFetch(url: string): Promise<Response> {
  let res = await fetch(url, { headers: githubHeaders() });

  if (!res.ok && process.env.GITHUB_API_TOKEN) {
    console.warn(
      `[github-api] ${res.status} with token, retrying without auth`,
    );

    res = await fetch(url, {
      headers: githubBaseHeaders(),
    });
  }

  if (!res.ok && res.status !== 404) {
    throw new Error(
      `[github-api] Request failed ${res.status}: ${url}`,
    );
  }

  return res;
}

Official Verdict

Bài viết không hề mất. File vẫn public. Tuy nhiên token bị hỏng. Logic retry cũ đã tin tưởng vào ngữ nghĩa của status code quá hẹp (401/403) và quên mất rằng CDN có thể hành xử khác đi (trả 404 để tránh lộ sự tồn tại của file private).

Mô hình nhận thức mới

Authentication cho public content nên là một sự tối ưu hoá (optimization), không phải là một sự phụ thuộc (dependency).

Tags
nextjsvercelgithub-apiproduction-debuggingmdx