Bỏ qua tới nội dung

Kỹ thuật

Theo dõi lỗi Cloudflare Workers: những lỗi vẫn trả về 200

Mình deploy một Worker hỏng theo năm kiểu khác nhau rồi xem client, Cloudflare và công cụ theo dõi lỗi mỗi bên thấy gì. Hai kiểu vẫn trả 200, và một trong hai không để lại dấu vết nào.

Andrew Babo7 phút đọc

Chia sẻ
Trong bài này
Theo dõi lỗi Cloudflare Workers: những lỗi vẫn trả về 200

Công cụ theo dõi lỗi bọc quanh một Cloudflare Worker báo những gì handler của bạn ném ra, không báo những gì handler giấu đi. Trong thí nghiệm của mình, một promise bị reject bên trong ctx.waitUntil vẫn trả 200 cho client, còn Cloudflare đánh dấu lần gọi là ok và chỉ đính kèm exception. Chính @belvador/cloudflare 0.4.0 của mình cũng không báo lỗi đó; từ bản 0.4.1 thì có.

Hai trường hợp vẫn cần code của bạn, dù dùng wrapper nào: lỗi bạn tự bắt rồi biến thành response 500, và promise không ai chờ. Phần còn lại của bài đi qua năm trường hợp mình thử và từng bên đã thấy gì.

Worker thử nghiệm#

Mình viết một Worker, mỗi route là một kiểu Worker hay hỏng:

  • /throw: handler fetch ném lỗi.
  • /caught-500: handler bắt lỗi rồi trả new Response(..., { status: 500 }).
  • /wait-until: handler trả ok và giao cho Cloudflare một promise sẽ reject sau 50 ms, qua ctx.waitUntil.
  • /floating: cùng lỗi reject đó, nhưng trong một promise không ai await và không ai đưa vào waitUntil.
  • scheduled(): handler cron ném lỗi.

Ngày 25/10/2026, mình chạy nó theo hai cách. Ở máy, một bản build không bọc SDK và một bản bọc @belvador/cloudflare 0.4.0 chạy trong workerd qua Miniflare 4.20260730.0, SDK gửi lỗi về một receiver trên 127.0.0.1. Sau đó mình deploy bản không bọc SDK thành một Worker tạm trên tài khoản của chính mình, gắn Tail để ghi lại góc nhìn của Cloudflare về từng lần gọi, rồi xoá Worker đi. Mỗi trường hợp chạy ba lần và lần nào cũng ra cùng kết quả.

Ai thấy lỗi nào#

Năm kiểu Worker hỏng, và client, Tail của Cloudflare, wrapper theo dõi lỗi có thấy từng kiểu không
Thí nghiệm 25/10/2026: mỗi trường hợp cho cùng kết quả ở 3/3 lần chạy
Trường hợpClient nhậnCloudflare (Tail)Wrapper (@belvador/cloudflare)
Handler ném lỗi500, trang lỗi của Cloudflareexception, có liệt kê lỗiđã báo
Bắt lỗi, trả 500500, body của bạnok, không có lỗikhông có gì
waitUntil bị reject200ok, có liệt kê lỗi0.4.0 không có gì; từ 0.4.1 đã báo
Promise thả nổi200ok, không có lỗikhông có gì
scheduled() ném lỗi—exceptionđã báo

Có ba điểm trong bảng này đáng nhìn kỹ.

Lỗi đã bắt chỉ còn là một mã trạng thái#

Khi code của bạn bắt lỗi rồi tự dựng response 500, không có gì bị ném ra cả. Cloudflare ghi lần gọi là ok, và wrapper không có exception nào để báo. Thứ duy nhất còn lại là con số 500 trong log request. Nếu muốn lỗi đó được theo dõi, hãy gọi captureException(error) ngay trong catch, trước khi dựng response. Tài liệu SDK Cloudflare có đúng mẫu đó.

Lỗi trong waitUntil nằm trong log của Cloudflare#

waitUntil cho phép việc tiếp tục chạy sau khi đã trả response; tài liệu context của Cloudflare cho nó tối đa 30 giây sau khi response được gửi hoặc client ngắt kết nối. Lúc promise reject thì response đã đi rồi, nên client thấy 200. Cloudflare có ghi exception vào lần gọi đó, nhưng kết quả vẫn là ok. Một dashboard hay cảnh báo chỉ đếm số lần gọi thất bại sẽ không đếm lần này.

Wrapper mình thử đầu tiên cũng không báo. Đó là lỗ hổng trong SDK của chính mình, không phải quy luật của Workers. Từ bản 0.4.1, @belvador/cloudflare báo promise waitUntil bị reject như một lỗi chưa xử lý, kèm scope của request đó, và Cloudflare vẫn nhận đúng lần reject như trước. Ngày 26/10/2026 mình chạy lại trường hợp này với 0.5.1 trong workerd ở máy: một event ở cả 3/3 lần, client vẫn nhận 200.

Nếu công cụ của bạn chưa làm vậy, hoặc bạn muốn lỗi được đánh dấu là đã xử lý kèm context của riêng bạn, hãy bắt ngay trong promise. Mẫu này được báo ở 3/3 lần chạy với cả hai phiên bản:

TypeScript
import { captureException, withBelvador } from "@belvador/cloudflare";export default withBelvador(() => ({}), {  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {    ctx.waitUntil(      sendReceipt(request).catch((error) => {        captureException(error); // response đã là 200; đây là bản báo lỗi duy nhất      }),    );    return new Response("ok");  },});

Đây là những gì Tail của Cloudflare ghi cho route đó, chỉ giữ các trường quan trọng:

Kết quả Tail cho route waitUntil: outcome ok, exception chỉ nằm trong danh sách
Lần gọi vẫn ok; lỗi chỉ nằm trong danh sách exceptions

Promise thả nổi không để lại gì để xem#

Trong bài thử, promise không được await và không được đưa vào waitUntil không để lại exception nào ở cả ba nơi: client, Tail và wrapper. Nhìn từ bên ngoài, mình còn không biết được đoạn code sau await có chạy hay không. Cloudflare ghi rõ waitUntil là cách để việc tiếp tục chạy sau response, nên hãy coi mọi promise thả nổi khác là việc có thể âm thầm không xảy ra. Đa số linter bắt được chúng (no-floating-promises trong typescript-eslint).

Checklist cho một Worker quan trọng#

  1. Bọc các handler. Việc này bắt được hai trường hợp có ném lỗi: fetch và scheduled. Handler queue, email và tail cũng bọc giống vậy.
  2. Báo những gì bạn tự bắt. Mọi catch có trả response lỗi nên gọi captureException trước.
  3. Đảm bảo mọi lần waitUntil bị reject đều được báo: bởi wrapper (Belvador từ 0.4.1) hoặc bởi một .catch() của bạn. Hãy coi nó như một job chạy nền, vì bản chất nó là vậy.
  4. Cấm promise thả nổi. await chúng hoặc đưa vào waitUntil.
  5. Giữ log của Cloudflare luôn bật. Workers Logs gồm log lần gọi, log tự ghi, lỗi và exception chưa bắt, và Cloudflare gợi ý xuất qua OpenTelemetry, Logpush hoặc Tail Workers để gửi đi nơi khác. Đó là nhân chứng thứ hai khi wrapper bỏ sót.
  6. Theo dõi giới hạn thời gian riêng. Worker hết thời gian CPU sẽ hỏng với lỗi 1102 của Cloudflare; Worker chạy cron có giới hạn 15 phút thời gian thực (giới hạn). Cả hai đều không phải exception trong code của bạn, nên hãy theo dõi chính lần chạy, như trong bài cron job chạy mãi không xong.

Belvador giúp ở đâu#

@belvador/cloudflare bọc các handler của Worker bạn export, cho mỗi lần gọi một scope riêng, báo những gì chúng ném ra và, từ 0.4.1, cả promise waitUntil bị reject, rồi ném lại để Cloudflare vẫn thấy đúng lỗi đó. Response của bạn được trả nguyên vẹn, và việc gửi lỗi diễn ra sau response nên không làm người dùng phải chờ. SDK không thấy được lỗi mà code của bạn tự bắt rồi biến thành response, cũng không thấy promise không ai chờ. Trang Cloudflare Workers cho thấy phần này đi cùng trace và cron monitor ra sao, còn hướng dẫn việc chạy nền nói về những job vẫn chạy sau khi request đã kết thúc.

Đọc tiếp#

Andrew Babo

Nhà sáng lập

Thuộc bài hướng dẫn: Giám sát AI agent chạy trên serverless function

Mọi bài viết

Hướng dẫn

Giám sát AI agent chạy trên serverless function

Một lần chạy agent trên serverless là một chuỗi lần gọi ngắn, mỗi lần có giới hạn thời gian riêng. Bài này chỉ ra cần theo dõi gì ở từng mắt xích, kèm ba thí nghiệm nhỏ cho thấy tín hiệu bị mất ở đâu.

Andrew Babo8 phút đọc

Đọc tiếp trong tài liệu

Xem lỗi production của chính bạn như thế này

Belvador chạy trên máy chủ Hetzner của chính bạn: lỗi, cron, uptime và trace — dữ liệu vẫn là của bạn.