Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions docs/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,6 @@ so publishing never reloads the admin app mid-test. The Vite dev proxy follows
the configured CMS `PORT`, keeping the Playwright admin UI pointed at the
disposable CMS instead of any regular dev server on port 3001.

When Vite itself runs on Bun, its Node-compatible native proxy can stop
draining multi-megabyte request bodies after socket backpressure fills both
sides. `largeBodyDevProxyPlugin` intercepts only known-length CMS API bodies of
at least 1 MiB, buffers them with the same 128 MiB ceiling as `Bun.serve`, and
forwards them with an explicit `Content-Length`. Small requests, non-CMS
traffic, and streaming AI responses remain on Vite's native proxy.

For debugging against a server you started yourself, set
`E2E_REUSE_SERVER=1` and override `E2E_ADMIN_BASE_URL` /
`E2E_PUBLIC_BASE_URL` as needed. Do not use reuse mode for CI or for
Expand Down
21 changes: 9 additions & 12 deletions docs/features/site-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,18 +594,15 @@ they commit: the `ColorInput` primitive throttles picker-drag change events
so a color drag cannot fill the socket backlog past the provider's send
gate.

In production the socket is same-origin. Under `vite dev` it is NOT: the
socket dials the CMS port directly, bypassing the Vite proxy
(`src/admin/pages/site/collab/socketUrl.ts`). `scripts/vite.ts` runs Vite
inside Bun, and Bun's `node:http` ClientRequest never emits `'upgrade'`, so a
proxied 101 takes the non-upgrade fallback: the browser socket hangs in
`readyState 0` forever — never opening, never closing, so the provider's
reconnect path is never even reached — and when that connection later ends,
the proxy's `socket.destroySoon()` call (an API Bun's socket lacks) throws
uncaught and kills the whole dev process. Only the PORT is swapped; the
hostname is preserved, because the session cookie is `SameSite=Lax` and
`localhost` ↔ `127.0.0.1` is a cross-site handshake that would drop it.
`devWorkflow.test.ts` gates the proxy against re-enabling `ws` forwarding.
The socket is same-origin in production and under `vite dev` alike: the
Vite proxy forwards the upgrade to the CMS (`ws: true` on the `/admin/api`
entry in `vite.config.ts`), so the provider simply dials
`window.location.host` + `SITE_SOCKET_PATH`. Before Bun 1.4.1 the
`node:http` client Vite runs on inside Bun never emitted `'upgrade'`, so the
socket had to dial the CMS port directly and the dev process could be killed
by a `socket.destroySoon()` call Bun lacked; 1.4.1 fixed both, and
`devWorkflow.test.ts` now gates the proxy the other way, requiring `ws`
forwarding to stay on.

**Presence** (`src/admin/pages/site/collab/awarenessState.ts`; per-frame
publishers in `collab/framePresencePublishers.ts`, rendering in
Expand Down
7 changes: 3 additions & 4 deletions scripts/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,9 @@ const processes: DevProcess[] = [
{
name: 'vite',
command: viteCommand('--host', '127.0.0.1', '--port', String(VITE_PORT), '--strictPort'),
// vite.config.ts reads PORT for both the proxy target and the collab
// socket's dev port. Inheriting it from the developer's shell happened to
// work only because CMS_PORT's default matches the config's — pass it
// explicitly so the two can't drift. `scripts/e2e-dev.ts` already does.
// vite.config.ts reads PORT for the proxy target. Inheriting it from the
// developer's shell happened to work only because CMS_PORT's default
// matches the config's; pass it explicitly so the two can't drift.
env: { PORT: String(CMS_PORT) },
},
]
Expand Down
116 changes: 0 additions & 116 deletions scripts/lib/largeBodyDevProxy.ts

This file was deleted.

78 changes: 0 additions & 78 deletions src/__tests__/collab/socketUrl.test.ts

This file was deleted.

20 changes: 8 additions & 12 deletions src/__tests__/devWorkflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,20 @@ describe('development workflow', () => {
expect(viteConfig).toContain("const CMS_DEV_SERVER_ORIGIN = `http://localhost:${process.env.PORT ?? '3001'}`")
expect(viteConfig).toContain('target: CMS_DEV_SERVER_ORIGIN')
expect(viteConfig).toContain('changeOrigin: true')
expect(viteConfig).toContain('largeBodyDevProxyPlugin()')
expect(viteConfig).toContain('shouldBufferLargeDevProxyRequest(req)')
})

it('Vite never forwards WebSocket upgrades, and the collab socket gets the CMS port instead', () => {
it('Vite forwards WebSocket upgrades, so the collab socket is same-origin in dev', () => {
const viteConfig = readSiteFile('vite.config.ts')
const devScript = readSiteFile('scripts/dev.ts')

// Vite runs inside Bun (scripts/vite.ts), and Bun's node:http client never
// emits 'upgrade'. Enabling `ws` forwarding makes the browser socket hang
// and then kills the dev process via `socket.destroySoon()`. The collab
// socket dials the CMS port directly instead — never re-enable this.
expect(viteConfig).not.toMatch(/^\s*ws:\s*true/m)
// Since Bun 1.4.1 the node:http client emits 'upgrade', so the collab
// socket rides the same proxy as every other /admin/api request and the
// dev server is same-origin like production. No port is dialled directly.
expect(viteConfig).toMatch(/^\s*ws:\s*true/m)
expect(viteConfig).not.toContain('VITE_CMS_DEV_PORT')

// The dialled port must come from the same source as the proxy target so
// they cannot drift, and dev.ts must actually hand PORT to the Vite child
// (it previously relied on both defaults happening to be 3001).
expect(viteConfig).toContain("'import.meta.env.VITE_CMS_DEV_PORT': JSON.stringify(process.env.PORT ?? '3001')")
// dev.ts must hand PORT to the Vite child so the proxy target cannot
// drift from the CMS it started (both defaults happen to be 3001).
expect(devScript).toContain('env: { PORT: String(CMS_PORT) }')
})

Expand Down
50 changes: 0 additions & 50 deletions src/__tests__/largeBodyDevProxy.test.ts

This file was deleted.

12 changes: 5 additions & 7 deletions src/admin/pages/site/collab/collabProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ import {
FRAME_SYNC,
PRESENCE_DOC_ID,
REMOTE_ORIGIN,
SITE_SOCKET_PATH,
decodeResetReason,
type ResetReason,
} from '@core/collab'
import { collabSocketUrl } from './socketUrl'

const RECONNECT_BASE_DELAY_MS = 1_000
const RECONNECT_MAX_DELAY_MS = 30_000
Expand Down Expand Up @@ -127,13 +127,11 @@ export function createCollabProvider(
const createSocket =
opts.createSocket ??
((): CollabSocketLike => {
// Under `vite dev` the socket bypasses the proxy and dials the CMS port
// directly — see socketUrl.ts for why, and why the hostname is kept.
const devCmsPort = import.meta.env.DEV
? String(import.meta.env.VITE_CMS_DEV_PORT ?? '3001')
: null
// Same origin in dev and production: under `vite dev` the proxy
// forwards the upgrade to the CMS (`ws: true` in vite.config.ts).
const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws'
return new WebSocket(
collabSocketUrl(window.location, devCmsPort),
`${scheme}://${window.location.host}${SITE_SOCKET_PATH}`,
) as unknown as CollabSocketLike
})

Expand Down
Loading
Loading