Skip to content

Restore Connect thread-loading speedup, without the gzip corruption - #915

Merged
SawyerHood merged 2 commits into
mainfrom
bb/restore-connect-performance-fix-safely-thr_meeeihbta8
Jul 30, 2026
Merged

SawyerHood merged 2 commits into
mainfrom
bb/restore-connect-performance-fix-safely-thr_meeeihbta8

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

Restores #909 (reverted in 17d652c) and fixes the response corruption that forced the revert.

The corruption

#909 lets the tunnel client forward the origin's compressed bytes verbatim. The gate then rebuilt those bytes into new Response objects under workerd's default encodeBody: "automatic", which means "this body is identity and I own the encoding" — so workerd dropped content-encoding and shipped gzip bytes labelled text/html. Browsers rendered raw gzip.

Two sites relayed pre-encoded bodies that way, and the correct fix is asymmetric:

Site Body it wraps Encoding
tunnel-do.ts resp-head raw origin bytes from tunnel frames manual — was corrupt
cache.ts hit Cache API stores bytes still compressed manual — was corrupt
cache.ts miss workerd content-decodes a subrequest body as the gate reads it automatic — already correct

Marking the miss path manual (the symmetric-looking fix) actively breaks it: it advertises gzip over plaintext and clients fail with Z_DATA_ERROR. The regression test catches that.

Regression test

apps/connect/src/response-encoding.test.ts drives the real TunnelDO and real serveWithCache inside workerd (miniflare), with a fake tunnel client relaying gzip frames over a real WebSocket. Node's Response ignores encodeBody, so a Node-only test would pass with or without this fix.

Mutation-checked: reverting the tunnel-do.ts call site fails 4 tests, reverting the cache.ts hit path fails 1. Two control routes pin workerd's default behaviour so the tests document the bug rather than merely guarding it.

Verification on real Cloudflare

Deployed to staging (bb-connect-staging, version 0946eaab, sawyer.vibecodethis.site). Because the tunnelled path needs a client + owner session, the end-to-end check ran against a throwaway workers.dev probe running this same TunnelDO + cache.ts, driven by the real TunnelSession client in front of the real built bb app (383 precompressed .gz assets, served as apps/server does). Probe deleted afterwards.

  • index.html: gzip-only client got 1509 raw bytes (1f8b0800) gunzipping to exactly 4255 bytes of real HTML; browser-style clients got it transcoded to zstd by the edge — which the edge can only do if it correctly understands the body as encoded.
  • 342 KB app bundle, miss → hit → hit: x-bb-cache: miss/hit plus cf-cache-status: HIT, every response gunzipping to sha256 18cb8f3a…, byte-identical to the origin file.
  • Legacy control at the edge reproduces the production symptom: content-encoding dropped, edge zstd-wraps the raw gzip, browser renders ^_M-^K^H… as text/html.
  • Accept-Encoding: identity: correct 4255 plain bytes, no encoding header.
  • Identity bodies measured through both paths are byte-identical (19984 → 4882 gzip, zstd for browser-style), so manual does not suppress edge compression for uncompressed origins — port shares and pre-Speed up thread loading over Connect #909 clients keep it.

Compatibility

Old client → new gate is safe: a pre-#909 client strips accept-encoding and drops content-encoding from resp-head, so its bodies are identity with no encoding header and manual asserts nothing false.

The broken direction is new client → old gate, which is unchanged by this PR: prod's gate must ship this before any released client carries the reapplied #909. Merging this triggers deploy-connect.yml, which deploys the prod gate — the correct order.

HOST_DAEMON_PROTOCOL_VERSION not bumped (stays 68): worker-only change, no daemon command / session payload / WS message altered, tunnel frame format unchanged (PROTOCOL_VERSION stays 1).

Tests

🤖 Generated with Claude Code

SawyerHood and others added 2 commits July 30, 2026 23:10
#909 lets the tunnel client forward the origin's compressed bytes
verbatim. The gate then rebuilt those bytes into new Response objects
under workerd's default encodeBody "automatic", which means "this body
is identity and I own the encoding" — workerd dropped content-encoding
and shipped gzip bytes labelled text/html, so browsers rendered raw
gzip. Two sites relayed pre-encoded bodies that way:

- TunnelDO's resp-head reconstruction (every tunnelled response), and
- the edge cache's hit path, which re-serves the still-compressed body
  the Cache API stored.

Both now build with encodeBody "manual". The cache miss path is
deliberately left alone: workerd content-decodes a subrequest body as
the gate reads it, so there the bytes really are identity and marking
them pre-encoded would advertise a gzip body that isn't gzipped.

The regression test drives the real TunnelDO and the real cache layer
inside workerd via miniflare, with a fake tunnel client relaying gzip
frames — Node's Response ignores encodeBody, so a Node-only test would
pass with or without the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SawyerHood
SawyerHood merged commit cf2f75f into main Jul 30, 2026
10 checks passed
@SawyerHood
SawyerHood deleted the bb/restore-connect-performance-fix-safely-thr_meeeihbta8 branch July 30, 2026 23:17
amadad pushed a commit to amadad/bb that referenced this pull request Aug 14, 2026
…et-bb#915)

Restores get-bb#909 (reverted in 17d652c) and fixes the response corruption
that forced the revert.

## The corruption

get-bb#909 lets the tunnel client forward the origin's compressed bytes
verbatim. The gate then rebuilt those bytes into new `Response` objects
under workerd's default `encodeBody: "automatic"`, which means *"this
body is identity and I own the encoding"* — so workerd dropped
`content-encoding` and shipped gzip bytes labelled `text/html`. Browsers
rendered raw gzip.

Two sites relayed pre-encoded bodies that way, and the correct fix is
**asymmetric**:

| Site | Body it wraps | Encoding |
|---|---|---|
| `tunnel-do.ts` resp-head | raw origin bytes from tunnel frames |
`manual` — was corrupt |
| `cache.ts` **hit** | Cache API stores bytes still compressed |
`manual` — was corrupt |
| `cache.ts` **miss** | workerd content-decodes a subrequest body as the
gate reads it | **automatic** — already correct |

Marking the miss path `manual` (the symmetric-looking fix) actively
breaks it: it advertises gzip over plaintext and clients fail with
`Z_DATA_ERROR`. The regression test catches that.

## Regression test

`apps/connect/src/response-encoding.test.ts` drives the **real
`TunnelDO` and real `serveWithCache`** inside workerd (miniflare), with
a fake tunnel client relaying gzip frames over a real WebSocket. Node's
`Response` ignores `encodeBody`, so a Node-only test would pass with or
without this fix.

Mutation-checked: reverting the `tunnel-do.ts` call site fails 4 tests,
reverting the `cache.ts` hit path fails 1. Two control routes pin
workerd's default behaviour so the tests document the bug rather than
merely guarding it.

## Verification on real Cloudflare

Deployed to staging (`bb-connect-staging`, version `0946eaab`,
`sawyer.vibecodethis.site`). Because the tunnelled path needs a client +
owner session, the end-to-end check ran against a throwaway workers.dev
probe running this same `TunnelDO` + `cache.ts`, driven by the **real
`TunnelSession` client** in front of the real built bb app (383
precompressed `.gz` assets, served as `apps/server` does). Probe deleted
afterwards.

- `index.html`: gzip-only client got 1509 raw bytes (`1f8b0800`)
gunzipping to exactly 4255 bytes of real HTML; browser-style clients got
it transcoded to zstd by the edge — which the edge can only do if it
correctly understands the body as encoded.
- 342 KB app bundle, miss → hit → hit: `x-bb-cache: miss`/`hit` plus
`cf-cache-status: HIT`, every response gunzipping to sha256 `18cb8f3a…`,
byte-identical to the origin file.
- Legacy control at the edge reproduces the production symptom:
`content-encoding` dropped, edge zstd-wraps the raw gzip, browser
renders `^_M-^K^H…` as `text/html`.
- `Accept-Encoding: identity`: correct 4255 plain bytes, no encoding
header.
- Identity bodies measured through both paths are byte-identical (19984
→ 4882 gzip, zstd for browser-style), so `manual` does **not** suppress
edge compression for uncompressed origins — port shares and pre-get-bb#909
clients keep it.

## Compatibility

Old client → new gate is safe: a pre-get-bb#909 client strips
`accept-encoding` and drops `content-encoding` from resp-head, so its
bodies are identity with no encoding header and `manual` asserts nothing
false.

The broken direction is **new client → old gate**, which is unchanged by
this PR: prod's gate must ship this before any released client carries
the reapplied get-bb#909. Merging this triggers `deploy-connect.yml`, which
deploys the prod gate — the correct order.

`HOST_DAEMON_PROTOCOL_VERSION` not bumped (stays 68): worker-only
change, no daemon command / session payload / WS message altered, tunnel
frame format unchanged (`PROTOCOL_VERSION` stays 1).

## Tests

- `@bb/connect` 88 ✅ + typecheck
- `@bb/tunnel-client` 6 ✅ + typecheck
- `bb-plugin-connect` 65 ✅
- `@bb/app` 28 focused (the four suites from get-bb#909) ✅ + typecheck

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant