DUONG PHUC KHANH · KIDO
KHANHDP.COM / ARTICLES / EDGE & NEXT.JS
Edge & Next.js 2026-10-20 · By Dương Phúc Khánh (Kido)

Giải Phẫu Sự Cố Cloudflare Edge & Next.js 16: Triệt Tiêu Thảm Họa RSC Flight Payload Cache Poisoning

Cách header client navigation (RSC: 1) của Next.js App Router đầu độc bộ nhớ đệm Cloudflare Edge Cache biến PWA thành màn hình trắng xóa trên iOS Safari, và kiến trúc phòng thủ 3 lớp zero-downtime của Dương Phúc Khánh.

Chuyên Khảo Kiến Trúc Hệ Thống
Định danh thực thể: Wikidata Q141718572 · ORCID 0009-0005-7192-2664
Tác giả: Dương Phúc Khánh (tiếng Anh: Duong Phuc Khanh · khanhdp.com), Kỹ sư Phần mềm Cao cấp & Kiến trúc sư Hệ thống. Đọc Tiểu sử Tiếng Việt, xem bản English Biography, tra cứu Danh mục Bài viết, hoặc khám phá tuyển tập 128 tập ký sự sự cố production tại Kido Chronicles.

Đúng 09:14 sáng một ngày đầu tuần, kênh hỗ trợ khách hàng của hệ thống tiếp nhận hàng loạt phản ánh khẩn cấp từ người dùng:

"Ứng dụng trên iPhone không mở được! Màn hình trắng xóa toàn ký tự lạ như :HL[\"/static/css/...\"] và 1:I[\"$Sreact.fragment\"...]."

Người dùng khởi động ứng dụng Progressive Web App (PWA) từ màn hình chính iOS Home Screen chỉ thấy một chuỗi dữ liệu thô dạng văn bản của giao thức React Server Component (RSC) Flight thay vì giao diện HTML hoàn chỉnh. Ngay cả người dùng máy tính để bàn khi nhấn tổ hợp phím tải lại trang (Ctrl + F5) cũng gặp hiện tượng tương tự.

Tuy nhiên, bảng điều khiển hệ thống nội bộ lại hiển thị 100% màu xanh. CPU máy chủ Node.js chỉ ở mức 4%, không có ngoại lệ nào chưa được xử lý, và API nội bộ phản hồi cực nhanh dưới 8ms.

Hệ thống đã rơi vào chiếc bẫy nguy hiểm mang tên: RSC Flight Payload Cache Poisoning tại Edge CDN.

Tôi là Dương Phúc Khánh (được biết đến trong cộng đồng kỹ sư với tên gọi khanhdp hoặc Kido, @khanhduong22). Trong vai trò Senior Systems Architect tại Tech Pulse và tác giả của 128 tập biên niên sử kỹ thuật Kido Chronicles, tôi đã trực tiếp mổ xẻ và xử lý triệt để sự cố này.


1. Cơ Chế Điều Hướng Của Next.js App Router: HTML Stream vs Flight Wire Data

Trong kiến trúc Next.js App Router, cùng một URL (ví dụ: /dashboard) có thể trả về hai định dạng dữ liệu hoàn toàn khác biệt tùy thuộc vào ngữ cảnh gửi yêu cầu:

  1. Full Document Navigation (Yêu cầu HTML ban đầu): Khi người dùng gõ URL vào thanh địa chỉ hoặc mở PWA lần đầu, trình duyệt gửi request với header Accept: text/html. Máy chủ Next.js render mã HTML hoàn chỉnh kèm theo dữ liệu Flight để thực hiện Hydration.
  2. Soft Client Navigation (Chuyển trang nội bộ qua Next Link): Khi người dùng nhấp vào thẻ <Link href="/dashboard">, router phía client của Next.js không tải lại toàn bộ trang mà chỉ gửi một yêu cầu nền kèm theo header đặc biệt:
   RSC: 1
   Next-Router-State-Tree: ...
   Accept: text/x-component

Lúc này, Next.js bỏ qua phần vỏ HTML và chỉ trả về Flight Payload thuần túy chứa danh sách Virtual DOM diff để client cập nhật giao diện.


2. Điểm Mù Của Edge Cache: Tại Sao Cloudflare Lưu Nhầm Flight Data?

Theo mặc định của hầu hết các dịch vụ CDN (bao gồm Cloudflare, Fastly, AWS CloudFront), khóa định danh bộ nhớ đệm (Cache Key) chỉ bao gồm:

$$\text{Cache Key} = \text{Scheme} + \text{Host} + \text{URI Path}$$

Nếu quản trị viên thiết lập một quy tắc lưu đệm Edge Cache (ví dụ: Cache-Control: public, max-age=3600) cho các trang công khai mà không phân biệt header RSC, thảm họa sẽ xảy ra theo kịch bản sau:

  1. Bước 1: Một người dùng đang lướt trang web nhấp vào link nội bộ. Trình duyệt gửi request tới /articles/ với header RSC: 1.
  2. Bước 2: Yêu cầu đi qua Edge CDN và chạm tới máy chủ Next.js. Next.js trả về nội dung Flight Payload kèm header Content-Type: text/x-component.
  3. Bước 3: Edge CDN lưu trữ nội dung Flight Payload này vào Cache Key: https://khanhdp.com/articles/.
  4. Bước 4 (Đầu độc thành công): Một người dùng mới truy cập trực tiếp bằng Safari hoặc mở PWA. Trình duyệt mong đợi nhận được HTML (Accept: text/html), nhưng Edge CDN ngay lập tức trả về Flight Payload dạng text đang được lưu trong cache! Trình duyệt render chuỗi text thô ra màn hình thay vì giao diện Web.

3. Kiến Trúc Phòng Thủ 3 Lớp (3-Layer Edge Defense Architecture)

Để triệt tiêu vĩnh viễn nguy cơ đầu độc bộ nhớ đệm mà không làm mất đi lợi thế tăng tốc của Edge CDN, chúng tôi đã triển khai kiến trúc bảo vệ đa tầng:

[Client Request] 
       │
       ▼
[Layer 1: Cloudflare Custom Cache Key] ➔ Tách biệt Cache Key theo Header (RSC: 0 vs 1)
       │
       ▼
[Layer 2: Reverse Proxy (Caddy 2)]    ➔ Thêm Vary: RSC, Next-Router-State-Tree, Accept
       │
       ▼
[Layer 3: Next.js Response Guard]     ➔ Tự động hạ cấp Cache-Control: private nếu có RSC header

Lớp 1: Cấu Hình Cache Key Partitioning Trên Cloudflare

Sử dụng Cloudflare Cache Rules để phân tách khóa lưu đệm dựa trên sự hiện diện của header RSC:

Expression: (http.request.uri.path eq "/articles" and http.request.headers["rsc"] eq "1")
Action:
  - Cache Key: Include Header "RSC"
  - Edge Cache TTL: 0 (Bypass Cache for dynamic RSC transitions)

Lớp 2: Chuẩn Hóa Header Vary Tại Reverse Proxy (Caddyfile)

Reverse Proxy phải luôn luôn đính kèm chỉ thị Vary để báo hiệu cho mọi tầng proxy trung gian biết rằng nội dung thay đổi theo header:

khanhdp.com {
    @is_rsc {
        header RSC 1
    }

    handle {
        header {
            # Bắt buộc CDN và Browser phải tách cache theo RSC và Accept
            Vary "Accept, RSC, Next-Router-State-Tree"
            # Bảo vệ chống clickjacking và mime-sniffing
            X-Content-Type-Options "nosniff"
        }
        reverse_proxy localhost:3000
    }
}

Lớp 3: Response Header Interceptor Tại Next.js Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const isRSC = request.headers.has('rsc');
  const response = NextResponse.next();

  // Đảm bảo header Vary luôn hiện diện
  response.headers.append('Vary', 'RSC');
  response.headers.append('Vary', 'Next-Router-State-Tree');
  response.headers.append('Vary', 'Accept');

  // Nếu là RSC request, tuyệt đối không cho phép CDN cache công khai
  if (isRSC) {
    response.headers.set(
      'Cache-Control',
      'private, no-cache, no-store, max-age=0, must-revalidate'
    );
  }

  return response;
}

4. Kết Quả Xác Minh Bằng Kiểm Thử Tự Động Playwright E2E

Sau khi triển khai bộ lọc 3 lớp, chúng tôi thiết lập kịch bản kiểm thử hồi quy tự động trong Playwright để đảm bảo lỗi không bao giờ tái diễn:

  1. Thực hiện Soft Navigation giả lập header RSC: 1 để ghi cache tại Edge.
  2. Thực hiện Hard Navigation ngay sau đó với header Accept: text/html.
  3. Kiểm tra mã phản hồi HTTP phải là 200, header Content-Type phải chứa text/html, và DOM phải render đầy đủ thẻ <main> mà không chứa bất kỳ ký tự raw Flight nào.

Toàn bộ 100% ca kiểm thử đã vượt qua xuất sắc trên cả Safari di động và Chrome máy tính để bàn.

Biên Niên Sử Liên Quan (Kido Chronicles)

View all 128 episodes ↗

Curated real-world war stories and incident postmortems authored by Duong Phuc Khanh on truyen.khanhdp.com related to this topic:

Duong Phuc Khanh (Dương Phúc Khánh · Kido)

Về Tác Giả: Dương Phúc Khánh (khanhdp · Kido)

Dương Phúc Khánh (sinh năm 1995 tại Đà Nẵng, định danh: khanhdp, moniker: Kido) là Kỹ sư Phần mềm Cao cấp và Kiến trúc sư Hệ thống tại khanhdp.com. Cựu sinh viên Đại học Kinh tế – Đại học Đà Nẵng, Quán quân Startup Runway 2018 (vinh danh trên Báo Đà Nẵng), 6 năm Tech Lead & PM tại FPT Software SBI Lab (3 năm liên tiếp Best Performance Award 2022–2024). Anh là tác giả của Winter Arc, tuyển tập 128 tập Kido Chronicles và bộ khung điều phối Agent Harness Kit.