<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Early Hints · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/early-hints/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/early-hints/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Lỗi 504 Cloudflare không phải sự cố thật: Early Hints và HTTP 103</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2026/cloudflare-504-early-hints/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2026/cloudflare-504-early-hints/</guid>
      <pubDate>Wed, 04 Nov 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Cloudflare</category>
      <category>Early Hints</category>
      <category>HTTP 103</category>
      <category>504 Gateway Timeout</category>
      <category>Hiệu năng web</category>
      <category>Preload</category>
      <category>Khả năng quan sát</category>
      <category>Gỡ lỗi</category>
      <description>Hàng nghìn lỗi 504 trên Cloudflare, nhưng không lỗi nào đến tay người dùng thật: tất cả đều là cache miss của Early Hints. Bài viết giải thích HTTP 103 hoạt động thế nào, cách phân biệt 504 giả với 504 thật, và header đã giải quyết vấn đề.</description>
      <content:encoded><![CDATA[<p>Bảng điều khiển của Cloudflare báo một phần tư số request của tôi là 504. Không một lỗi nào trong số đó đến tay người dùng: chúng là cache miss của tính năng Early Hints trên Cloudflare, được ghi thành 504 với <code>ClientRequestSource = earlyHintsCache</code> và <code>OriginResponseStatus = 0</code>. Bài này nói cách phân biệt chúng với 504 thật, và một header duy nhất biến những lần miss đó thành hit.</p>
<p>Tôi gặp chúng lần đầu vào tháng 9, khi <a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-09-09-scale-to-zero-504s/" >bảng điều khiển Cloudflare báo 22% request bị lỗi</a> trong khi mọi health check vẫn xanh; tôi tắt Early Hints và chuyển sang việc khác. Đến tháng 11, cũng zone đó lại có thêm 2.600 lỗi 504 trong một ngày, Early Hints lại đang bật, và lần này tôi muốn biết chính xác chúng là gì, cũng như cách đúng là tắt tính năng thêm lần nữa hay làm cho nó thực sự làm đúng việc. Zone này đứng trước một origin trên Fly.io, phục vụ một Django API, một web app viết bằng Expo và một trang marketing dựng bằng Hugo.</p>

<h2 class="relative group">Câu trả lời ngắn
    <div id="câu-trả-lời-ngắn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a2u-tr%e1%ba%a3-l%e1%bb%9di-ng%e1%ba%afn" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<ul>
<li>Những lỗi 504 này được ghi bởi bộ cache Early Hints của Cloudflare, không phải bởi khách truy cập. Trong log của Cloudflare, chúng có <code>ClientRequestSource = earlyHintsCache</code>.</li>
<li>Ở đây, 504 nghĩa là cache miss: Cloudflare không có gợi ý nào được lưu cho URL đó. Một lần hit được ghi là 2xx.</li>
<li>Chúng không bao giờ chạm tới origin của bạn. <code>OriginResponseStatus</code> là <code>0</code>.</li>
<li>Early Hints chạy bằng response header HTTP <code>Link</code>. Các thẻ <code>&lt;link rel=&quot;preload&quot;&gt;</code> trong HTML không cung cấp dữ liệu cho nó.</li>
<li>Gửi header đó đi, và trên những trang có thứ cần preload, các lần miss sẽ chuyển thành hit và bắt đầu có phản hồi <code>103 Early Hints</code> thật.</li>
</ul>
<p>Có thêm ba điều đáng biết ngay từ đầu. Chỉ gợi ý CSS, script và font quan trọng cho việc render, và giữ <code>crossorigin</code> cho font. Kiểm tra bằng <code>curl -sv --http2</code>: bạn sẽ thấy <code>103</code> trước <code>200</code>. Và dù quyết định thế nào, hãy để <code>earlyHintsCache</code> ra ngoài hệ thống cảnh báo; nếu bạn sẽ không bao giờ gửi header, hãy tắt Early Hints thay vì trả giá bằng tiếng ồn trong log.</p>

<h2 class="relative group">Cách phân biệt 504 Cloudflare giả với 504 thật
    <div id="cách-phân-biệt-504-cloudflare-giả-với-504-thật" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1ch-ph%c3%a2n-bi%e1%bb%87t-504-cloudflare-gi%e1%ba%a3-v%e1%bb%9bi-504-th%e1%ba%adt" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Nếu bạn dùng Logpush hoặc Instant Logs, hãy xem hai trường trước tiên. <code>ClientRequestSource</code> là <code>eyeball</code> với request của khách truy cập thật, và là <code>earlyHintsCache</code> với các lượt tra cứu Early Hints. <code>OriginResponseStatus</code> bằng <code>0</code> nghĩa là origin chưa từng được liên hệ.</p>
<p>Trong GraphQL Analytics API, sự phân tách đó chính là dimension <code>requestSource</code>. Đây là truy vấn tôi luôn chạy trước khi tin vào bất kỳ đợt tăng vọt 5xx nào:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-graphql" data-lang="graphql"><span class="line"><span class="cl"><span class="kd">query</span><span class="w"> </span><span class="p">(</span><span class="nv">$zone</span><span class="p">:</span><span class="w"> </span><span class="nc">String</span><span class="p">!,</span><span class="w"> </span><span class="nv">$since</span><span class="p">:</span><span class="w"> </span><span class="nc">Time</span><span class="p">!)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="py">viewer</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="py">zones</span><span class="p">(</span><span class="py">filter</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="py">zoneTag</span><span class="p">:</span><span class="w"> </span><span class="nv">$zone</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="py">httpRequestsAdaptiveGroups</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="py">limit</span><span class="p">:</span><span class="w"> </span><span class="nc">20</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="py">filter</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="py">datetime_geq</span><span class="p">:</span><span class="w"> </span><span class="nv">$since</span><span class="p">,</span><span class="w"> </span><span class="nc">edgeResponseStatus</span><span class="p">:</span><span class="w"> </span><span class="nc">504</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="py">orderBy</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="py">count_DESC</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="py">count</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="py">dimensions</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="py">requestSource</span><span class="w"> </span><span class="py">userAgent</span><span class="w"> </span><span class="py">clientRequestPath</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Nhóm theo User-Agent cũng cho ra cùng một câu chuyện. Vào tháng 9, các lượt tra cứu Early Hints hiện lên dưới dạng <code>bastion early hints</code> và <code>nginx-ssl early hints</code>. Đến tháng 11 thì là <code>fl2 early hints</code> và <code>nginx-ssl early hints</code> (FL2 là thế hệ proxy mới của Cloudflare). Trong 24 giờ, toàn bộ 2.631 lỗi 504 của zone đều đến từ những User-Agent này. Không có lỗi nào đến từ trình duyệt, ứng dụng, curl hay crawler của công cụ tìm kiếm.</p>
<p>Một lỗi 504 thật mà khách truy cập nhìn thấy thì hầu như luôn đến từ phía bạn, sau Cloudflare: một proxy hay load balancer đứng trước ứng dụng đã chờ quá lâu và bỏ cuộc. Đó có thể là nginx, một load balancer trên cloud, hoặc proxy của nền tảng đang chờ một máy đang ngủ khởi động. Các timeout của chính Cloudflare dùng mã khác: <strong>522</strong> nghĩa là không kết nối được tới origin, còn <strong>524</strong> nghĩa là đã kết nối nhưng không nhận được phản hồi trong vòng 125 giây (mặc định trên các gói dưới Enterprise).</p>
<p>Vậy nếu bạn tìm đến đây vì muốn tăng timeout của Cloudflare do lỗi 504: timeout đứng sau một lỗi 504 gần như không bao giờ là của Cloudflare. Hãy kiểm tra <code>proxy_read_timeout</code> trong nginx, idle timeout của load balancer, hoặc thời gian nền tảng của bạn cần để đánh thức một máy. Một manh mối hữu ích: nếu request thất bại ở một con số tròn thấp hơn hẳn 125 giây, như 30 hay 60, thì đó là giới hạn của proxy của bạn, không phải của Cloudflare. (Chỉ gói Enterprise mới nâng được giới hạn 125 giây của Cloudflare, tối đa 6.000 giây, và chính Cloudflare cũng coi đó là giải pháp tạm thời.)</p>

<h2 class="relative group">Bản chất thật của những lỗi 504 này
    <div id="bản-chất-thật-của-những-lỗi-504-này" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#b%e1%ba%a3n-ch%e1%ba%a5t-th%e1%ba%adt-c%e1%bb%a7a-nh%e1%bb%afng-l%e1%bb%97i-504-n%c3%a0y" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Trên một zone đã bật Early Hints, mỗi request tới một trang HTML hoặc một URL không có phần mở rộng (trên một zone nặng về API thì phần lớn lời gọi API đều thuộc diện này) đều kèm thêm một lượt tra cứu nội bộ: Cloudflare kiểm tra cache của chính nó xem có gợi ý nào để gửi không. Lượt tra cứu đó được ghi thành một dòng riêng, với <code>ClientRequestSource = earlyHintsCache</code>. Nó không phải request của người dùng cuối, và không bao giờ được gửi tới origin của bạn.</p>
<p>Mã trạng thái trên dòng đó là kết quả tra cứu cache, không phải lỗi:</p>
<ul>
<li>2xx là hit. Cloudflare có lưu header <code>Link</code> cho URL đó và đã gửi chúng tới trình duyệt dưới dạng <code>103</code>. Trên zone của tôi, hit xuất hiện dưới cả hai mã <code>200</code> và <code>204</code>.</li>
<li>504 là miss. Không có gì được lưu, nên không có gì để gửi sớm.</li>
</ul>
<p>Nếu origin không bao giờ gửi header mà Early Hints dựa vào, mọi lượt tra cứu đều miss và mọi request đều ghi một dòng 504. Trong ba giờ trước khi tôi thay đổi bất cứ điều gì, zone ghi nhận 173 lượt tra cứu Early Hints và cả 173 đều là miss. Kể từ khi được bật, tính năng này chưa từng gửi đi một gợi ý nào.</p>
<p>Một điều chỉnh cho bài viết tháng 9 của tôi: khi đó tôi mô tả chúng là các probe &ldquo;không nhận được phản hồi từ origin trên Fly&rdquo;. Điều đó không hoàn toàn đúng. Chúng hoàn toàn không đi tới origin. Mã 504 ghi lại một lần cache miss, không phải một lần timeout.</p>
<p>Cloudflare có nói ngắn gọn về chuyện này trong <a href="https://developers.cloudflare.com/cache/advanced-configuration/early-hints/"  target="_blank" rel="noreferrer">tài liệu Early Hints</a>: một lượt tra cứu Early Hints được ghi với <code>ClientRequestSource: earlyHintsCache</code> là &ldquo;expected and benign&rdquo; (nằm trong dự kiến và vô hại), 200 là cache hit còn 504 là cache miss, và trong Log Explorer bạn lọc chúng ra bằng <code>ClientRequestSource != &quot;earlyHintsCache&quot;</code>. Cache Analytics vốn đã bỏ chúng ra ngoài. Còn biểu đồ lưu lượng HTTP làm tôi hoảng thì không.</p>

<h2 class="relative group">Lỗi 504 giả đã khiến tôi nâng cấp máy chủ
    <div id="lỗi-504-giả-đã-khiến-tôi-nâng-cấp-máy-chủ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#l%e1%bb%97i-504-gi%e1%ba%a3-%c4%91%c3%a3-khi%e1%ba%bfn-t%c3%b4i-n%c3%a2ng-c%e1%ba%a5p-m%c3%a1y-ch%e1%bb%a7" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Đây là lý do việc tách bạch quan trọng. Ngày 3 tháng 11, một ngày trước khi tôi bắt tay vào bài này, Cloudflare ghi nhận 3.292 lỗi 504 trên khoảng 13.300 request của khách truy cập: tỷ lệ lỗi bề ngoài là 25%. API chạy trên máy <code>shared-cpu-1x</code> của Fly.io, loại máy hoạt động theo burst credit: nó chỉ được bảo đảm một phần nhỏ của CPU và phải tiêu credit để chạy nhanh hơn. Một bài kiểm thử tải hồi tháng 8 cho thấy khi tải kéo dài, máy bị bóp xuống khoảng 20 đến 30 request mỗi giây. &ldquo;Một phần tư số request bị timeout vào giờ cao điểm&rdquo; trông đúng y như kiểu bóp CPU đó, nên tôi chuyển API sang <code>shared-cpu-2x</code>.</p>
<p>Nhưng tất cả những lỗi 504 đó đều là các lần miss của cache Early Hints. Khi tách theo <code>requestSource</code> và xem lại log của chính origin:</p>
<ul>
<li>Mọi lỗi 5xx của Cloudflare từ 28/10 đến 4/11 đều là lượt tra cứu <code>earlyHintsCache</code>, trừ khoảng một lỗi 525 mỗi ngày (lỗi 525 tôi lần ra đến từ một trình quét bảo mật).</li>
<li>Log truy cập của origin từ 24/10 đến 4/11 không có lỗi 5xx nào, và chỉ có một request chậm hơn 10 giây.</li>
<li>Số liệu của Fly cho thấy lúc cao nhất cũng chỉ khoảng 7 request mỗi giây, và máy chưa bao giờ dùng quá một phần năm của một lõi CPU. Nó chưa bao giờ tiến gần mức trần bị bóp.</li>
</ul>
<p>Máy lớn hơn chẳng sửa được gì, vì vốn không có gì hỏng. API đã quay lại <code>shared-cpu-1x</code>.</p>
<p>Đừng ra quyết định về năng lực xử lý dựa trên một biểu đồ 5xx chưa được tách theo request source. Hãy đối chiếu cả với log của chính origin: nếu Cloudflare báo một phần tư số request thất bại mà máy chủ của bạn không ghi nhận lỗi nào, hãy tìm hiểu Cloudflare đang đếm cái gì trước khi động vào máy chủ.</p>
<p><em>Cập nhật ngày 5/11/2026: phiên bản đầu của phần này viết rằng tuần đó API thực sự đã sập trên <code>shared-cpu-1x</code>. Thực ra không phải vậy. Các số liệu ở trên là những gì tôi tìm thấy khi kiểm tra lại.</em></p>

<h2 class="relative group">HTTP 103 có phải là lỗi không?
    <div id="http-103-có-phải-là-lỗi-không" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#http-103-c%c3%b3-ph%e1%ba%a3i-l%c3%a0-l%e1%bb%97i-kh%c3%b4ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Không. Các mã trạng thái trong dải 1xx là phản hồi thông tin: những phản hồi tạm thời mà máy chủ có thể gửi trước phản hồi thật. <code>103 Early Hints</code>, được định nghĩa trong RFC 8297, mang theo header <code>Link</code> để trình duyệt có thể bắt đầu tải CSS, JavaScript và font quan trọng, hoặc mở sẵn kết nối tới các origin khác, trong khi máy chủ vẫn đang dựng trang. Mã <code>200</code> cuối cùng sẽ đến sau trên cùng một request.</p>
<p>Mức hỗ trợ không đồng đều nhưng an toàn. Các trình duyệt dựa trên Chromium (Chrome từ phiên bản 103, và Edge) cùng Firefox bản mới xử lý cả gợi ý <code>preload</code> lẫn <code>preconnect</code>. Safari (từ phiên bản 17) chỉ dùng <code>preconnect</code>. Trình duyệt chỉ xử lý <code>103</code> qua HTTP/2 và HTTP/3. Trình duyệt nào không hiểu nó thì đơn giản là chờ <code>200</code> như mọi khi, nên <code>103</code> là một lợi thế xuất phát sớm cho phần lớn khách truy cập và không ảnh hưởng gì tới số còn lại.</p>

<h2 class="relative group">Vì sao của tôi trống trơn: header <code>Link</code> so với <code>&lt;link rel=preload&gt;</code>
    <div id="vì-sao-của-tôi-trống-trơn-header-link-so-với-link-relpreload" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ac-sao-c%e1%bb%a7a-t%c3%b4i-tr%e1%bb%91ng-tr%c6%a1n-header-link-so-v%e1%bb%9bi-link-relpreload" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Đây là chỗ tôi mất nhiều thời gian nhất, vì trông nó như thể lẽ ra đã hoạt động rồi.</p>
<p>Early Hints chỉ cache đúng một thứ: <strong>response header HTTP <code>Link</code></strong> có quan hệ <code>rel=preload</code> hoặc <code>rel=preconnect</code>. Cloudflare đọc nó từ các phản hồi thông thường của bạn (mã <code>200</code>, <code>301</code> hoặc <code>302</code>, trên URL HTML hoặc không có phần mở rộng), lưu lại, rồi phát lại cho khách truy cập tiếp theo dưới dạng <code>103</code> trước khi phản hồi đầy đủ sẵn sàng.</p>
<p>Các trang của tôi vốn đã có preload, nhưng ở dạng thẻ trong <code>&lt;head&gt;</code> của HTML:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;preload&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/_expo/static/css/global-7d77bd.css&#34;</span> <span class="na">as</span><span class="o">=</span><span class="s">&#34;style&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;preload&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/_expo/static/js/web/entry-fd7fa4.js&#34;</span> <span class="na">as</span><span class="o">=</span><span class="s">&#34;script&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;preload&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/fonts/manrope-latin.woff2&#34;</span> <span class="na">as</span><span class="o">=</span><span class="s">&#34;font&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;font/woff2&#34;</span> <span class="na">crossorigin</span><span class="p">&gt;</span></span></span></code></pre></div></div>
<p>Những thẻ này vẫn có tác dụng: trình duyệt hành động theo chúng. Nhưng trình duyệt chỉ thấy chúng sau khi đã nhận HTML và bắt đầu phân tích, mà đó lại chính là khoảnh khắc Early Hints muốn đi trước. Bản export của Expo và theme Hugo tôi dùng đều tự viết các thẻ này, nhưng không cái nào đặt header. Vậy là tính năng đã bật, preload cũng đã khai báo, nhưng hai thứ chưa bao giờ gặp nhau.</p>

<h2 class="relative group">Cách sửa: gửi header từ dữ liệu bạn vốn đã có
    <div id="cách-sửa-gửi-header-từ-dữ-liệu-bạn-vốn-đã-có" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1ch-s%e1%bb%ada-g%e1%bb%adi-header-t%e1%bb%ab-d%e1%bb%af-li%e1%bb%87u-b%e1%ba%a1n-v%e1%bb%91n-%c4%91%c3%a3-c%c3%b3" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Cách sạch nhất không phải là tự tay duy trì một danh sách tài nguyên quan trọng, thứ sẽ lỗi thời mỗi khi hash của bundle thay đổi. Mỗi trang đã tự khai báo những gì nó muốn preload, trong chính các thẻ đó. Vậy chỉ cần chép chúng sang header <code>Link</code> khi phục vụ trang. <code>html.parser</code> trong thư viện chuẩn xử lý được cả thuộc tính có và không có dấu ngoặc kép (minifier của Hugo bỏ dấu ngoặc đi), nên không có regex nào để viết sai:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">html.parser</span> <span class="kn">import</span> <span class="n">HTMLParser</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PreloadLinks</span><span class="p">(</span><span class="n">HTMLParser</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">hints</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">handle_starttag</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="n">attrs</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">a</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">attrs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">tag</span> <span class="o">!=</span> <span class="s2">&#34;link&#34;</span> <span class="ow">or</span> <span class="s2">&#34;preload&#34;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;rel&#34;</span><span class="p">)</span> <span class="ow">or</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">split</span><span class="p">()</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">a</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;href&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="cl">        <span class="n">hint</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;&lt;</span><span class="si">{</span><span class="n">a</span><span class="p">[</span><span class="s1">&#39;href&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&gt;; rel=preload&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">a</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;as&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">hint</span> <span class="o">+=</span> <span class="sa">f</span><span class="s2">&#34;; as=</span><span class="si">{</span><span class="n">a</span><span class="p">[</span><span class="s1">&#39;as&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">a</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;as&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;font&#34;</span> <span class="ow">or</span> <span class="s2">&#34;crossorigin&#34;</span> <span class="ow">in</span> <span class="n">a</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">hint</span> <span class="o">+=</span> <span class="s2">&#34;; crossorigin&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">hints</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">hint</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">link_header</span><span class="p">(</span><span class="n">html</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">limit</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">12</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">PreloadLinks</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">feed</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;, &#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">dict</span><span class="o">.</span><span class="n">fromkeys</span><span class="p">(</span><span class="n">parser</span><span class="o">.</span><span class="n">hints</span><span class="p">))[:</span><span class="n">limit</span><span class="p">])</span> <span class="ow">or</span> <span class="kc">None</span></span></span></code></pre></div></div>
<p>Và gắn nó vào phản hồi HTML trong Django:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="n">content_type</span><span class="o">=</span><span class="s2">&#34;text/html&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">link</span> <span class="o">:=</span> <span class="n">link_header</span><span class="p">(</span><span class="n">html</span><span class="p">))</span> <span class="ow">and</span> <span class="s2">&#34;Link&#34;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">response</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span><span class="p">[</span><span class="s2">&#34;Link&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">link</span></span></span></code></pre></div></div>
<p>Có một chi tiết đã làm tôi vấp, và đáng để nhớ: giữ <code>crossorigin</code> cho font. Một preload font thiếu nó sẽ bị coi là một request khác, font bị tải về hai lần, và trang chậm đi chứ không nhanh hơn.</p>
<p>Bạn không cần tự viết <code>103</code>. Không có đoạn mã phản hồi tạm thời nào phải thêm vào ASGI, Granian hay uvicorn. Cloudflare dựng <code>103</code> từ header của bạn; việc duy nhất của bạn là gửi header đó.</p>

<h2 class="relative group">Trên các stack khác
    <div id="trên-các-stack-khác" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#tr%c3%aan-c%c3%a1c-stack-kh%c3%a1c" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Quy tắc ở đâu cũng vậy: đặt header <code>Link</code> lên phản hồi HTML và để Cloudflare lo phần còn lại.</p>
<ul>
<li>
<p>nginx, trong location phục vụ HTML:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">location</span> <span class="p">=</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">add_header</span> <span class="s">Link</span> <span class="s">&#34;&lt;/css/app.css&gt;</span><span class="p">;</span> <span class="kn">rel=preload</span><span class="p">;</span> <span class="kn">as=style&#34;</span> <span class="s">always</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">add_header</span> <span class="s">Link</span> <span class="s">&#34;&lt;/fonts/inter.woff2&gt;</span><span class="p">;</span> <span class="kn">rel=preload</span><span class="p">;</span> <span class="kn">as=font</span><span class="p">;</span> <span class="kn">crossorigin&#34;</span> <span class="s">always</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
</li>
<li>
<p>Laravel dùng Vite đã có sẵn middleware cho việc này, <code>AddLinkHeadersForPreloadedAssets</code>, chuyển các preload của Vite thành header <code>Link</code>. Hai vấn đề từng được báo cáo với nó cũng chính là những cái bẫy ở trên: các gợi ý <code>modulepreload</code> của nó từng được gửi đi mà không có <code>crossorigin</code>, khiến trình duyệt có thể bỏ qua chúng và tải script hai lần, và trên các trang lớn, header từng vượt quá giới hạn header khoảng 8 KB của Apache. Hãy kiểm tra cả hai trên phiên bản bạn đang dùng.</p>
</li>
<li>
<p>Mọi môi trường khác: dùng một header <code>Link</code> phân tách bằng dấu phẩy, hoặc gửi nhiều header <code>Link</code>. Cách nào cũng được.</p>
</li>
</ul>
<p>Chỗ mọi người hay làm quá tay là chọn gợi ý những gì. Chỉ gợi ý CSS quan trọng cho việc render, script đầu vào và một hai font dùng ở màn hình đầu tiên, đừng nhét mọi chunk vào. Danh sách dài khiến trình duyệt đi tải những thứ chưa cần trước cả những thứ đang cần. Tôi giới hạn ở mười hai mục, và đó là vì bản export của Expo đã chọn lọc sẵn danh sách preload.</p>
<p>Nếu bạn không đứng sau Cloudflare, hoặc CDN của bạn không hỗ trợ Early Hints, header <code>Link</code> vẫn giúp được một chút: trình duyệt có thể bắt đầu tải các tài nguyên đó ngay khi header phản hồi tới nơi, trước khi phân tích phần thân. Nhưng sẽ không có <code>103</code> nào trừ khi máy chủ hoặc CDN của bạn tự gửi.</p>

<h2 class="relative group">Cách kiểm tra xem nó đã hoạt động chưa
    <div id="cách-kiểm-tra-xem-nó-đã-hoạt-động-chưa" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1ch-ki%e1%bb%83m-tra-xem-n%c3%b3-%c4%91%c3%a3-ho%e1%ba%a1t-%c4%91%e1%bb%99ng-ch%c6%b0a" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p><code>curl</code> cho bạn thấy <code>103</code> trực tiếp. Gửi request bằng HTTP/2 và chỉ giữ lại các dòng trạng thái cùng header <code>Link</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ curl -sv --http2 -o /dev/null https://example.com/register 2&gt;&amp;1 | grep -E &#39;^&lt; (HTTP|link)&#39;
</span></span><span class="line"><span class="cl">&lt; HTTP/2 103
</span></span><span class="line"><span class="cl">&lt; link: &lt;/_expo/static/css/reset-f5d396.css&gt;; as=style; rel=preload, …
</span></span><span class="line"><span class="cl">&lt; HTTP/2 200
</span></span><span class="line"><span class="cl">&lt; link: &lt;/_expo/static/css/reset-f5d396.css&gt;; rel=preload; as=style, …</span></span></code></pre></div></div>
<p><code>103</code> đến trước, rồi mới tới <code>200</code>. Cloudflare sắp xếp lại thứ tự các thuộc tính trong gợi ý, điều này vô hại. Đừng kỳ vọng mọi request đều nhận được <code>103</code>. Trong ba lần đầu của tôi, chỉ có hai lần nhận được. Lần truy cập đầu tiên sau khi thay đổi chỉ lưu header lại, và Cloudflare bỏ qua <code>103</code> khi header của phản hồi cuối cùng đã sẵn sàng.</p>
<p>Sau đó quay lại truy vấn phân tích ở trên, lọc theo <code>requestSource: &quot;earlyHintsCache&quot;</code>, nhóm theo mã trạng thái và đường dẫn. Trên những trang bạn đã sửa, hit sẽ dần thay thế miss.</p>

<h2 class="relative group">Điều gì đã thay đổi, và điều gì còn lại
    <div id="điều-gì-đã-thay-đổi-và-điều-gì-còn-lại" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91i%e1%bb%81u-g%c3%ac-%c4%91%c3%a3-thay-%c4%91%e1%bb%95i-v%c3%a0-%c4%91i%e1%bb%81u-g%c3%ac-c%c3%b2n-l%e1%ba%a1i" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Vài phút sau khi header được đưa lên production, zone ghi nhận những lần hit Early Hints đầu tiên trong lịch sử, và <code>curl</code> cho thấy phản hồi <code>103</code> thật trên các trang web. Một header giải quyết cùng lúc hai vấn đề: tính năng bắt đầu hoạt động, và một phần các &ldquo;lỗi 504&rdquo; biến mất.</p>
<p>Điều còn lại là một JSON API chẳng có gì để preload. Các endpoint API tiếp tục miss và tiếp tục ghi các dòng <code>504</code>, và trên một zone nặng về API thì phần này vẫn chiếm đa số. Bạn có ba lựa chọn hợp lý. Gửi header trên những trang có lợi và chấp nhận các đường dẫn API tiếp tục ghi miss; tính năng hoạt động, các biểu đồ yên tĩnh hơn nhưng không hoàn toàn im lặng. Loại <code>earlyHintsCache</code> khỏi cảnh báo 5xx và dashboard, dù bạn quyết định gì đi nữa, vì một lượt tra cứu cache nội bộ không bao giờ nên đánh thức ai dậy. Hoặc tắt Early Hints (Speed → Settings → Content Optimization trong dashboard, hoặc thiết lập <code>early_hints</code> của zone qua API) nếu bạn sẽ không bao giờ gửi header <code>Link</code>; một ứng dụng một trang có HTML rất nhỏ có thể không được lợi gì nhiều, và khi tắt thì mọi lượt tra cứu dừng lại, tiếng ồn cũng biến mất theo.</p>
<p>Tôi giữ nó bật. Các trang web giờ đã được xuất phát sớm hơn, các lần miss của API đã bị lọc khỏi mọi thứ có thể kích hoạt cảnh báo, và đó mới là điều lẽ ra tôi phải làm vào tháng 9 thay vì tắt tính năng đi.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/cloudflare-504-early-hints/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
