Skip to content

Commit bc4b293

Browse files
authored
feat(dev): use web fetch handler rather than proxy (#1051)
1 parent f4749f8 commit bc4b293

File tree

10 files changed

+388
-95
lines changed

10 files changed

+388
-95
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ nuxt-app
77
.pnpm-store
88
coverage
99
stats.json
10+
playground-bun
11+
playground-deno
12+
playground-node

knip.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"fuse.js",
3333
"giget",
3434
"h3",
35-
"httpxy",
3635
"jiti",
3736
"nypm",
3837
"ofetch",
@@ -42,6 +41,7 @@
4241
"pkg-types",
4342
"scule",
4443
"semver",
44+
"srvx",
4545
"ufo",
4646
"youch"
4747
]

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
"std-env": "^3.9.0",
3737
"tinyexec": "^1.0.1",
3838
"typescript": "^5.9.2",
39-
"undici": "^7.16.0",
4039
"vitest": "^3.2.4",
4140
"vue": "^3.5.21"
4241
},

packages/nuxi/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"get-port-please": "^3.2.0",
5656
"giget": "^2.0.0",
5757
"h3": "^1.15.4",
58-
"httpxy": "^0.1.7",
5958
"jiti": "^2.6.0",
6059
"listhen": "^1.9.0",
6160
"magicast": "^0.3.5",
@@ -70,11 +69,13 @@
7069
"rollup-plugin-visualizer": "^6.0.3",
7170
"scule": "^1.3.0",
7271
"semver": "^7.7.2",
72+
"srvx": "^0.8.7",
7373
"std-env": "^3.9.0",
7474
"tinyexec": "^1.0.1",
7575
"typescript": "^5.9.2",
7676
"ufo": "^1.6.1",
7777
"unbuild": "^3.6.1",
78+
"undici": "^6.21.3",
7879
"unplugin-purge-polyfills": "^0.1.0",
7980
"vitest": "^3.2.4",
8081
"youch": "^4.1.0-beta.11"

packages/nuxi/src/commands/dev.ts

Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@ import type { NuxtOptions } from '@nuxt/schema'
22
import type { ParsedArgs } from 'citty'
33
import type { HTTPSOptions, ListenOptions } from 'listhen'
44
import type { ChildProcess } from 'node:child_process'
5-
import type { IncomingMessage, ServerResponse } from 'node:http'
6-
import type { TLSSocket } from 'node:tls'
75
import type { NuxtDevContext, NuxtDevIPCMessage } from '../dev/utils'
86

97
import { fork } from 'node:child_process'
108
import process from 'node:process'
119

1210
import { defineCommand } from 'citty'
1311
import { isSocketSupported } from 'get-port-please'
14-
import { createProxyServer } from 'httpxy'
1512
import { listen } from 'listhen'
1613
import { getArgs as getListhenArgs, parseArgs as parseListhenArgs } from 'listhen/cli'
1714
import { resolve } from 'pathe'
@@ -20,8 +17,10 @@ import { isBun, isDeno, isTest } from 'std-env'
2017

2118
import { initialize } from '../dev'
2219
import { renderError } from '../dev/error'
20+
import { createFetchHandler } from '../dev/fetch'
2321
import { isSocketURL, parseSocketURL } from '../dev/socket'
2422
import { resolveLoadingTemplate } from '../dev/utils'
23+
import { connectToChildNetwork, connectToChildSocket } from '../dev/websocket'
2524
import { showVersions } from '../utils/banner'
2625
import { overrideEnv } from '../utils/env'
2726
import { loadKit } from '../utils/kit'
@@ -131,14 +130,14 @@ const command = defineCommand({
131130
}
132131
}
133132

134-
// Start proxy Listener
135-
const devProxy = await createDevProxy(cwd, nuxtOptions, listenOptions)
133+
// Start listener
134+
const devHandler = await createDevHandler(cwd, nuxtOptions, listenOptions)
136135

137136
const nuxtSocketEnv = process.env.NUXT_SOCKET ? process.env.NUXT_SOCKET === '1' : undefined
138137

139138
const useSocket = nuxtSocketEnv ?? (nuxtOptions._majorVersion === 4 && await isSocketSupported())
140139

141-
const urls = await devProxy.listener.getURLs()
140+
const urls = await devHandler.listener.getURLs()
142141
// run initially in in no-fork mode
143142
const { onRestart, onReady, close } = await initialize({
144143
cwd,
@@ -147,16 +146,16 @@ const command = defineCommand({
147146
public: listenOptions.public,
148147
publicURLs: urls.map(r => r.url),
149148
proxy: {
150-
url: devProxy.listener.url,
149+
url: devHandler.listener.url,
151150
urls,
152-
https: devProxy.listener.https,
153-
addr: devProxy.listener.address,
151+
https: devHandler.listener.https,
152+
addr: devHandler.listener.address,
154153
},
155154
// if running with nuxt v4 or `NUXT_SOCKET=1`, we use the socket listener
156155
// otherwise pass 'true' to listen on a random port instead
157156
}, {}, useSocket ? undefined : true)
158157

159-
onReady(address => devProxy.setAddress(address))
158+
onReady(address => devHandler.setAddress(address))
160159

161160
// ... then fall back to pre-warmed fork if a hard restart is required
162161
const fork = startSubprocess(cwd, ctx.args, ctx.rawArgs, listenOptions)
@@ -165,16 +164,16 @@ const command = defineCommand({
165164
fork,
166165
devServer.close().catch(() => {}),
167166
])
168-
await subprocess.initialize(devProxy, useSocket)
167+
await subprocess.initialize(devHandler, useSocket)
169168
})
170169

171170
return {
172-
listener: devProxy.listener,
171+
listener: devHandler.listener,
173172
async close() {
174173
await close()
175174
const subprocess = await fork
176175
subprocess.kill(0)
177-
await devProxy.listener.close()
176+
await devHandler.listener.close()
178177
},
179178
}
180179
},
@@ -189,42 +188,46 @@ type ArgsT = Exclude<
189188
undefined | ((...args: unknown[]) => unknown)
190189
>
191190

192-
type DevProxy = Awaited<ReturnType<typeof createDevProxy>>
191+
type DevHandler = Awaited<ReturnType<typeof createDevHandler>>
193192

194-
async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial<ListenOptions>) {
193+
async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial<ListenOptions>) {
195194
let loadingMessage = 'Nuxt dev server is starting...'
196195
let error: Error | undefined
197196
let address: string | undefined
198197

199198
let loadingTemplate = nuxtOptions.devServer.loadingTemplate
200199

201-
const proxy = createProxyServer({})
202-
203-
proxy.on('proxyReq', (proxyReq, req) => {
204-
if (!proxyReq.hasHeader('x-forwarded-for')) {
205-
const address = req.socket.remoteAddress
206-
if (address) {
207-
proxyReq.appendHeader('x-forwarded-for', address)
200+
// Create fetch-based handler
201+
const fetchHandler = createFetchHandler(
202+
() => {
203+
if (!address) {
204+
return undefined
208205
}
209-
}
210-
if (!proxyReq.hasHeader('x-forwarded-port')) {
211-
const localPort = req?.socket?.localPort
212-
if (localPort) {
213-
proxyReq.setHeader('x-forwarded-port', req.socket.localPort)
206+
207+
// Convert address string to DevAddress format
208+
if (isSocketURL(address)) {
209+
const { socketPath } = parseSocketURL(address)
210+
return { socketPath }
214211
}
215-
}
216-
if (!proxyReq.hasHeader('x-forwarded-Proto')) {
217-
const encrypted = (req?.connection as TLSSocket)?.encrypted
218-
proxyReq.setHeader('x-forwarded-proto', encrypted ? 'https' : 'http')
219-
}
220-
})
221212

222-
const listener = await listen((req: IncomingMessage, res: ServerResponse) => {
223-
if (error) {
213+
// Parse network address
214+
try {
215+
const url = new URL(address)
216+
return {
217+
host: url.hostname,
218+
port: Number.parseInt(url.port) || 80,
219+
}
220+
}
221+
catch {
222+
return undefined
223+
}
224+
},
225+
// Error handler
226+
async (req, res) => {
224227
renderError(req, res, error)
225-
return
226-
}
227-
if (!address) {
228+
},
229+
// Loading handler
230+
async (req, res) => {
228231
res.statusCode = 503
229232
res.setHeader('Content-Type', 'text/html')
230233
res.setHeader('Cache-Control', 'no-store')
@@ -239,10 +242,10 @@ async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptio
239242
res.end(loadingTemplate({ loading: loadingMessage }))
240243
}
241244
return resolveLoadingMessage()
242-
}
243-
const target = isSocketURL(address) ? parseSocketURL(address) : address
244-
proxy.web(req, res, { target })
245-
}, listenOptions)
245+
},
246+
)
247+
248+
const listener = await listen(fetchHandler, listenOptions)
246249

247250
listener.server.on('upgrade', (req, socket, head) => {
248251
if (!address) {
@@ -251,13 +254,23 @@ async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptio
251254
}
252255
return
253256
}
254-
const target = isSocketURL(address) ? parseSocketURL(address) : address
255-
// @ts-expect-error TODO: fix socket type in httpxy
256-
return proxy.ws(req, socket, { target, xfwd: true }, head).catch(() => {
257-
if (!socket.destroyed) {
258-
socket.end()
257+
if (isSocketURL(address)) {
258+
const { socketPath } = parseSocketURL(address)
259+
connectToChildSocket(socketPath, req, socket, head)
260+
}
261+
else {
262+
try {
263+
const url = new URL(address)
264+
const host = url.hostname
265+
const port = Number.parseInt(url.port) || 80
266+
connectToChildNetwork(host, port, req, socket, head)
259267
}
260-
})
268+
catch {
269+
if (!socket.destroyed) {
270+
socket.end()
271+
}
272+
}
273+
}
261274
})
262275

263276
return {
@@ -279,7 +292,7 @@ async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptio
279292

280293
async function startSubprocess(cwd: string, args: { logLevel: string, clear: boolean, dotenv: string, envName: string, extends?: string }, rawArgs: string[], listenOptions: Partial<ListenOptions>) {
281294
let childProc: ChildProcess | undefined
282-
let devProxy: DevProxy
295+
let devHandler: DevHandler
283296
let ready: Promise<void> | undefined
284297
const kill = (signal: NodeJS.Signals | number) => {
285298
if (childProc) {
@@ -288,9 +301,9 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
288301
}
289302
}
290303

291-
async function initialize(proxy: DevProxy, socket: boolean) {
292-
devProxy = proxy
293-
const urls = await devProxy.listener.getURLs()
304+
async function initialize(handler: DevHandler, socket: boolean) {
305+
devHandler = handler
306+
const urls = await devHandler.listener.getURLs()
294307
await ready
295308
childProc!.send({
296309
type: 'nuxt:internal:dev:context',
@@ -302,16 +315,16 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
302315
public: listenOptions.public,
303316
publicURLs: urls.map(r => r.url),
304317
proxy: {
305-
url: devProxy.listener.url,
318+
url: devHandler.listener.url,
306319
urls,
307-
https: devProxy.listener.https,
320+
https: devHandler.listener.https,
308321
},
309322
} satisfies NuxtDevContext,
310323
})
311324
}
312325

313326
async function restart() {
314-
devProxy?.clearError()
327+
devHandler?.clearError()
315328
// Kill previous process with restart signal (not supported on Windows)
316329
if (process.platform === 'win32') {
317330
kill('SIGTERM')
@@ -343,19 +356,19 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
343356
resolve()
344357
}
345358
else if (message.type === 'nuxt:internal:dev:ready') {
346-
devProxy.setAddress(message.address)
359+
devHandler.setAddress(message.address)
347360
if (startTime) {
348361
logger.debug(`Dev server ready for connections in ${Date.now() - startTime}ms`)
349362
}
350363
}
351364
else if (message.type === 'nuxt:internal:dev:loading') {
352-
devProxy.setAddress(undefined)
353-
devProxy.setLoadingMessage(message.message)
354-
devProxy.clearError()
365+
devHandler.setAddress(undefined)
366+
devHandler.setLoadingMessage(message.message)
367+
devHandler.clearError()
355368
}
356369
else if (message.type === 'nuxt:internal:dev:loading:error') {
357-
devProxy.setAddress(undefined)
358-
devProxy.setError(message.error)
370+
devHandler.setAddress(undefined)
371+
devHandler.setError(message.error)
359372
}
360373
else if (message.type === 'nuxt:internal:dev:restart') {
361374
restart()

0 commit comments

Comments
 (0)