Skip to content

Commit 678348a

Browse files
committed
fix(dev): drain connections before force-closing the dev server
1 parent d5c583e commit 678348a

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

packages/nuxt-cli/src/dev/listen.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ const ANY_HOSTS = new Set(['', '0.0.0.0', '::'])
6969
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1'])
7070
const DEFAULT_PORTS = { http: 80, https: 443 } as const
7171

72+
/** How long in-flight requests have to finish before their sockets are killed. */
73+
const CONNECTION_DRAIN_TIMEOUT_MS = 1000
74+
7275
/**
7376
* Render a URL for display: the port is omitted when it is the protocol
7477
* default, and percent-encoding is decoded so a non-ASCII `baseURL` is
@@ -270,8 +273,23 @@ export async function listen(handler: RequestListener, options: ListenOptions =
270273
close: async () => {
271274
await tunnel?.close()
272275
return new Promise<void>((resolve, reject) => {
273-
server.close(error => (error ? reject(error) : resolve()))
274-
server.closeAllConnections?.()
276+
let forceClose: NodeJS.Timeout | undefined
277+
server.close((error) => {
278+
if (forceClose) {
279+
clearTimeout(forceClose)
280+
}
281+
if (error) {
282+
reject(error)
283+
}
284+
else {
285+
resolve()
286+
}
287+
})
288+
// Sockets waiting on keep-alive are closed at once, so shutdown is only
289+
// delayed while a request is actually being served.
290+
server.closeIdleConnections?.()
291+
forceClose = setTimeout(() => server.closeAllConnections?.(), CONNECTION_DRAIN_TIMEOUT_MS)
292+
forceClose.unref()
275293
})
276294
},
277295
}

packages/nuxt-cli/test/unit/listen.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,35 @@ describe('listen', () => {
202202
})
203203
})
204204

205+
describe('listener.close', () => {
206+
it('should let an in-flight request finish', async () => {
207+
let respond: (() => void) | undefined
208+
const listener = await listen((_req, res) => {
209+
respond = () => res.end('done')
210+
}, { port: 0, hostname: '127.0.0.1', showURL: false })
211+
212+
const response = fetch(listener.url)
213+
await vi.waitFor(() => expect(respond).toBeDefined())
214+
215+
const closed = listener.close()
216+
respond!()
217+
218+
await expect((await response).text()).resolves.toBe('done')
219+
await closed
220+
})
221+
222+
it('should not wait for idle keep-alive connections', async () => {
223+
const listener = await listen((_req, res) => res.end('ok'), { port: 0, hostname: '127.0.0.1', showURL: false })
224+
225+
await fetch(listener.url).then(response => response.text())
226+
227+
const start = Date.now()
228+
await listener.close()
229+
230+
expect(Date.now() - start).toBeLessThan(500)
231+
})
232+
})
233+
205234
describe('copyURL', () => {
206235
afterEach(() => {
207236
restoreEnvironment()

0 commit comments

Comments
 (0)