Skip to content

Commit 3afd02b

Browse files
authored
fix(dev): use proxy instead of web fetch handler (#1104)
1 parent caa1a6d commit 3afd02b

File tree

7 files changed

+89
-817
lines changed

7 files changed

+89
-817
lines changed

knip.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"fuse.js",
3434
"giget",
3535
"h3-next",
36+
"http-proxy-3",
3637
"jiti",
3738
"nitro",
3839
"nitropack",

packages/nuxi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"giget": "^2.0.0",
5151
"h3": "^1.15.4",
5252
"h3-next": "npm:h3@^2.0.1-rc.4",
53+
"http-proxy-3": "^1.22.0",
5354
"jiti": "^2.6.1",
5455
"listhen": "^1.9.0",
5556
"magicast": "^0.3.5",
@@ -71,7 +72,6 @@
7172
"tsdown": "^0.15.9",
7273
"typescript": "^5.9.3",
7374
"ufo": "^1.6.1",
74-
"undici": "^7.16.0",
7575
"unplugin-purge-polyfills": "^0.1.0",
7676
"vitest": "^3.2.4",
7777
"youch": "^4.1.0-beta.11"

packages/nuxi/src/commands/dev.ts

Lines changed: 58 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import type { NuxtOptions } from '@nuxt/schema'
22
import type { ParsedArgs } from 'citty'
3+
import type { ProxyTargetDetailed } from 'http-proxy-3/dist/lib/http-proxy'
34
import type { HTTPSOptions, ListenOptions } from 'listhen'
45
import type { ChildProcess } from 'node:child_process'
6+
import type { IncomingMessage, ServerResponse } from 'node:http'
7+
import type { TLSSocket } from 'node:tls'
58
import type { NuxtDevContext, NuxtDevIPCMessage } from '../dev/utils'
69

710
import { fork } from 'node:child_process'
811
import process from 'node:process'
912

1013
import { defineCommand } from 'citty'
1114
import { isSocketSupported } from 'get-port-please'
15+
import { createProxyServer } from 'http-proxy-3'
1216
import { listen } from 'listhen'
1317
import { getArgs as getListhenArgs, parseArgs as parseListhenArgs } from 'listhen/cli'
1418
import { resolve } from 'pathe'
@@ -17,10 +21,8 @@ import { isBun, isDeno, isTest } from 'std-env'
1721

1822
import { initialize } from '../dev'
1923
import { renderError } from '../dev/error'
20-
import { createFetchHandler } from '../dev/fetch'
2124
import { isSocketURL, parseSocketURL } from '../dev/socket'
2225
import { resolveLoadingTemplate } from '../dev/utils'
23-
import { connectToChildNetwork, connectToChildSocket } from '../dev/websocket'
2426
import { showVersionsFromConfig } from '../utils/banner'
2527
import { overrideEnv } from '../utils/env'
2628
import { loadKit } from '../utils/kit'
@@ -131,14 +133,14 @@ const command = defineCommand({
131133
}
132134
}
133135

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

137139
const nuxtSocketEnv = process.env.NUXT_SOCKET ? process.env.NUXT_SOCKET === '1' : undefined
138140

139141
const useSocket = nuxtSocketEnv ?? (nuxtOptions._majorVersion === 4 && await isSocketSupported())
140142

141-
const urls = await devHandler.listener.getURLs()
143+
const urls = await devProxy.listener.getURLs()
142144
// run initially in in no-fork mode
143145
const { onRestart, onReady, close } = await initialize({
144146
cwd,
@@ -147,16 +149,16 @@ const command = defineCommand({
147149
public: listenOptions.public,
148150
publicURLs: urls.map(r => r.url),
149151
proxy: {
150-
url: devHandler.listener.url,
152+
url: devProxy.listener.url,
151153
urls,
152-
https: devHandler.listener.https,
153-
addr: devHandler.listener.address,
154+
https: devProxy.listener.https,
155+
addr: devProxy.listener.address,
154156
},
155157
// if running with nuxt v4 or `NUXT_SOCKET=1`, we use the socket listener
156158
// otherwise pass 'true' to listen on a random port instead
157159
}, {}, useSocket ? undefined : true)
158160

159-
onReady(address => devHandler.setAddress(address))
161+
onReady(address => devProxy.setAddress(address))
160162

161163
// ... then fall back to pre-warmed fork if a hard restart is required
162164
const fork = startSubprocess(cwd, ctx.args, ctx.rawArgs, listenOptions)
@@ -165,16 +167,16 @@ const command = defineCommand({
165167
fork,
166168
devServer.close().catch(() => {}),
167169
])
168-
await subprocess.initialize(devHandler, useSocket)
170+
await subprocess.initialize(devProxy, useSocket)
169171
})
170172

171173
return {
172-
listener: devHandler.listener,
174+
listener: devProxy.listener,
173175
async close() {
174176
await close()
175177
const subprocess = await fork
176178
subprocess.kill(0)
177-
await devHandler.listener.close()
179+
await devProxy.listener.close()
178180
},
179181
}
180182
},
@@ -189,53 +191,42 @@ type ArgsT = Exclude<
189191
undefined | ((...args: unknown[]) => unknown)
190192
>
191193

192-
type DevHandler = Awaited<ReturnType<typeof createDevHandler>>
194+
type DevProxy = Awaited<ReturnType<typeof createDevProxy>>
193195

194-
async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial<ListenOptions>) {
196+
async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial<ListenOptions>) {
195197
let loadingMessage = 'Nuxt dev server is starting...'
196198
let error: Error | undefined
197199
let address: string | undefined
198200

199201
let loadingTemplate = nuxtOptions.devServer.loadingTemplate
200202

201-
// Create fetch-based handler
202-
const fetchHandler = createFetchHandler(
203-
() => {
204-
if (!address) {
205-
return undefined
206-
}
207-
208-
// Convert address string to DevAddress format
209-
if (isSocketURL(address)) {
210-
const { socketPath } = parseSocketURL(address)
211-
return { socketPath }
212-
}
203+
const proxy = createProxyServer({})
213204

214-
// Parse network address
215-
try {
216-
const url = new URL(address)
217-
return {
218-
host: url.hostname,
219-
port: Number.parseInt(url.port) || 80,
220-
}
205+
proxy.on('proxyReq', (proxyReq, req) => {
206+
if (!proxyReq.hasHeader('x-forwarded-for')) {
207+
const address = req.socket.remoteAddress
208+
if (address) {
209+
proxyReq.appendHeader('x-forwarded-for', address)
221210
}
222-
catch {
223-
return undefined
224-
}
225-
},
226-
// Error handler
227-
async (req, res) => {
228-
renderError(req, res, error)
229-
},
230-
// Loading handler
231-
async (req, res) => {
232-
if (res.headersSent) {
233-
if (!res.writableEnded) {
234-
res.end()
235-
}
236-
return
211+
}
212+
if (!proxyReq.hasHeader('x-forwarded-port')) {
213+
const localPort = req?.socket?.localPort
214+
if (localPort) {
215+
proxyReq.setHeader('x-forwarded-port', localPort)
237216
}
217+
}
218+
if (!proxyReq.hasHeader('x-forwarded-Proto')) {
219+
const encrypted = (req?.connection as TLSSocket)?.encrypted
220+
proxyReq.setHeader('x-forwarded-proto', encrypted ? 'https' : 'http')
221+
}
222+
})
238223

224+
const listener = await listen((req: IncomingMessage, res: ServerResponse) => {
225+
if (error) {
226+
renderError(req, res, error)
227+
return
228+
}
229+
if (!address) {
239230
res.statusCode = 503
240231
res.setHeader('Content-Type', 'text/html')
241232
res.setHeader('Cache-Control', 'no-store')
@@ -250,10 +241,10 @@ async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOpt
250241
res.end(loadingTemplate({ loading: loadingMessage }))
251242
}
252243
return resolveLoadingMessage()
253-
},
254-
)
255-
256-
const listener = await listen(fetchHandler, listenOptions)
244+
}
245+
const target = isSocketURL(address) ? parseSocketURL(address) as ProxyTargetDetailed : address
246+
proxy.web(req, res, { target })
247+
}, listenOptions)
257248

258249
listener.server.on('upgrade', (req, socket, head) => {
259250
if (!address) {
@@ -262,23 +253,8 @@ async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOpt
262253
}
263254
return
264255
}
265-
if (isSocketURL(address)) {
266-
const { socketPath } = parseSocketURL(address)
267-
connectToChildSocket(socketPath, req, socket, head)
268-
}
269-
else {
270-
try {
271-
const url = new URL(address)
272-
const host = url.hostname
273-
const port = Number.parseInt(url.port) || 80
274-
connectToChildNetwork(host, port, req, socket, head)
275-
}
276-
catch {
277-
if (!socket.destroyed) {
278-
socket.end()
279-
}
280-
}
281-
}
256+
const target = isSocketURL(address) ? parseSocketURL(address) as ProxyTargetDetailed : address
257+
return proxy.ws(req, socket, head, { target, xfwd: true })
282258
})
283259

284260
return {
@@ -300,7 +276,7 @@ async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOpt
300276

301277
async function startSubprocess(cwd: string, args: { logLevel: string, clear: boolean, dotenv: string, envName: string, extends?: string }, rawArgs: string[], listenOptions: Partial<ListenOptions>) {
302278
let childProc: ChildProcess | undefined
303-
let devHandler: DevHandler
279+
let devProxy: DevProxy
304280
let ready: Promise<void> | undefined
305281
const kill = (signal: NodeJS.Signals | number) => {
306282
if (childProc) {
@@ -309,9 +285,9 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
309285
}
310286
}
311287

312-
async function initialize(handler: DevHandler, socket: boolean) {
313-
devHandler = handler
314-
const urls = await devHandler.listener.getURLs()
288+
async function initialize(proxy: DevProxy, socket: boolean) {
289+
devProxy = proxy
290+
const urls = await devProxy.listener.getURLs()
315291
await ready
316292
childProc!.send({
317293
type: 'nuxt:internal:dev:context',
@@ -323,16 +299,16 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
323299
public: listenOptions.public,
324300
publicURLs: urls.map(r => r.url),
325301
proxy: {
326-
url: devHandler.listener.url,
302+
url: devProxy.listener.url,
327303
urls,
328-
https: devHandler.listener.https,
304+
https: devProxy.listener.https,
329305
},
330306
} satisfies NuxtDevContext,
331307
})
332308
}
333309

334310
async function restart() {
335-
devHandler?.clearError()
311+
devProxy?.clearError()
336312
if (!globalThis.__nuxt_cli__) {
337313
return
338314
}
@@ -367,19 +343,19 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
367343
resolve()
368344
}
369345
else if (message.type === 'nuxt:internal:dev:ready') {
370-
devHandler.setAddress(message.address)
346+
devProxy.setAddress(message.address)
371347
if (startTime) {
372348
logger.debug(`Dev server ready for connections in ${Date.now() - startTime}ms`)
373349
}
374350
}
375351
else if (message.type === 'nuxt:internal:dev:loading') {
376-
devHandler.setAddress(undefined)
377-
devHandler.setLoadingMessage(message.message)
378-
devHandler.clearError()
352+
devProxy.setAddress(undefined)
353+
devProxy.setLoadingMessage(message.message)
354+
devProxy.clearError()
379355
}
380356
else if (message.type === 'nuxt:internal:dev:loading:error') {
381-
devHandler.setAddress(undefined)
382-
devHandler.setError(message.error)
357+
devProxy.setAddress(undefined)
358+
devProxy.setError(message.error)
383359
}
384360
else if (message.type === 'nuxt:internal:dev:restart') {
385361
restart()

0 commit comments

Comments
 (0)