Public: anyone can read this; the crew can change it.
A fragment's tag rides in its markup too: Cloudflare strips etag from HTML #324
Check · Review · Land · Live done — Landed and live in production.
fragment-tag-in-markup → main 1 commit
4 files
changed +24
−7
docs/STUDIO.md +4 −2
| @@ -327,8 +327,10 @@ everything the viewer may see) and works like this: | ||
| 327 | 327 | fetch of the page; that is the fallback, not the design. |
| 328 | 328 | - **Nothing painted that did not change.** A fragment's `etag` is a hash of |
| 329 | 329 | its bytes; the client sends it back as `if-none-match` and an unchanged |
| 330 | region is a 304 with no body. When bytes do arrive, two renders that | |
| 331 | differ only in a relative time ("3m ago") are the same region. | |
| 330 | region is a 304 with no body. The same tag is stamped on the fragment's | |
| 331 | first element (`data-etag`), because a proxy may drop the header — | |
| 332 | Cloudflare strips `etag` from HTML. When bytes do arrive, two renders | |
| 333 | that differ only in a relative time ("3m ago") are the same region. | |
| 332 | 334 | - **One fetch per burst.** Events within a beat of each other cost one |
| 333 | 335 | fetch per address, never more often than every second and a half; a |
| 334 | 336 | hidden tab fetches nothing and catches up on the first look back. |
src/web/client.ts +4 −1
| @@ -2077,9 +2077,12 @@ export const liveRegionsJs = /* js */ ` | ||
| 2077 | 2077 | if (text === null) return; |
| 2078 | 2078 | if (own) { |
| 2079 | 2079 | // A fragment's icons come inlined (layout.fragment), so it needs |
| 2080 | // nothing from this page's sprite. | |
| 2080 | // nothing from this page's sprite. Its tag is stamped on it too, | |
| 2081 | // for when a proxy between us dropped the header. | |
| 2081 | 2082 | var holder = document.createElement('template'); |
| 2082 | 2083 | holder.innerHTML = text; |
| 2084 | var stamped = holder.content.querySelector('[data-etag]'); | |
| 2085 | if (stamped) tags[src] = '"' + stamped.getAttribute('data-etag') + '"'; | |
| 2083 | 2086 | swap(holder.content, ids); |
| 2084 | 2087 | return; |
| 2085 | 2088 | } |
src/web/layout.ts +13 −4
| @@ -646,6 +646,12 @@ export function redirect( | ||
| 646 | 646 | * paint. `cache-control: no-store` keeps the browser's own cache out of it — |
| 647 | 647 | * the live client holds the tag itself and asks conditionally on purpose, |
| 648 | 648 | * so an unchanged region costs one round trip and no repaint. |
| 649 | * | |
| 650 | * The same tag is stamped on the fragment's first element as `data-etag`, | |
| 651 | * because a proxy in between may drop the header: Cloudflare strips `etag` | |
| 652 | * from HTML it reserves the right to rewrite, and did on the live forge. | |
| 653 | * The hash is of the unstamped body, so the server recomputes it the same | |
| 654 | * way next time; the request header is what it compares. | |
| 649 | 655 | */ |
| 650 | 656 | export function fragment( |
| 651 | 657 | body: Html, |
| @@ -661,11 +667,14 @@ export function fragment( | ||
| 661 | 667 | "cache-control": "no-store", |
| 662 | 668 | }; |
| 663 | 669 | if (status !== 200) return new Response(text, { status, headers }); |
| 664 | const etag = `"${sha256(text).slice(0, 16)}"`; | |
| 665 | headers.etag = etag; | |
| 666 | if (o.request?.headers.get("if-none-match") === etag) | |
| 670 | const hash = sha256(text).slice(0, 16); | |
| 671 | headers.etag = `"${hash}"`; | |
| 672 | if (o.request?.headers.get("if-none-match") === headers.etag) | |
| 667 | 673 | return new Response(null, { status: 304, headers }); |
| 668 | return new Response(text, { status, headers }); | |
| 674 | return new Response( | |
| 675 | text.replace(/^(\s*<[a-zA-Z][^\s>]*)/, `$1 data-etag="${hash}"`), | |
| 676 | { status, headers }, | |
| 677 | ); | |
| 669 | 678 | } |
| 670 | 679 | |
| 671 | 680 | /** Redirect back to a page carrying a message — the shape most handlers want. */ |
test/studio.test.ts +3
| @@ -2683,6 +2683,9 @@ describe("the app's live fragment: /:owner/:name/app/state", () => { | ||
| 2683 | 2683 | const etag = first.headers.get("etag"); |
| 2684 | 2684 | expect(etag).toMatch(/^"[0-9a-f]{16}"$/); |
| 2685 | 2685 | const text = await first.text(); |
| 2686 | // The tag rides in the markup as well, for a proxy that drops the header. | |
| 2687 | expect(text).toMatch(/^\s*<div data-etag="[0-9a-f]{16}"/); | |
| 2688 | expect(`"${/data-etag="([0-9a-f]{16})"/.exec(text)![1]}"`).toBe(etag!); | |
| 2686 | 2689 | expect(text).toContain('id="live-now"'); |
| 2687 | 2690 | expect(text).toContain('data-live="run.*,pull.*,issue.*"'); |
| 2688 | 2691 | expect(text).toContain('data-live-src="/alice/vault/app/now"'); |
new above
offline