Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cd7850a
feat: implement NITRO_UNIX_SOCKET and graceful shutdown env vars
wadefletch Feb 9, 2026
4218d87
docs: add env var documentation to bun and deno runtime pages
wadefletch Feb 9, 2026
3482064
docs(node): fix handler preset name and export
wadefletch Feb 10, 2026
853f67f
fix: call runtime `close` hook on shutdown signals
wadefletch Feb 10, 2026
ea4235a
docs: clarify close hook availability across preset types
wadefletch Feb 10, 2026
63e2ebd
feat: combine graceful shutdown and env var changes
wadefletch Feb 10, 2026
77f90e4
revert: remove unrelated node middleware handler docs change
wadefletch Feb 10, 2026
70627b2
fix: make shutdown hook handler async with error handling
wadefletch Feb 10, 2026
6261cd6
refactor: extract resolveGracefulShutdownConfig helper
wadefletch Feb 10, 2026
8e1f996
chore: apply automated updates
autofix-ci[bot] Feb 10, 2026
5ebc5f9
refactor: clean up shutdown module and format preset entries
wadefletch Feb 10, 2026
ad08c19
chore: apply automated updates
autofix-ci[bot] Feb 10, 2026
b25ed2f
chore: apply project formatting and fix lint warning
wadefletch Feb 10, 2026
8769db6
fix: guard process.on behind runtime check, add await regression test
wadefletch Feb 10, 2026
2127dc3
fix: use strict === "true" check for NITRO_SHUTDOWN_DISABLED
wadefletch Feb 10, 2026
4b68824
chore: apply automated updates
autofix-ci[bot] Feb 10, 2026
8fc90dd
test: combine NITRO_SHUTDOWN_DISABLED tests into it.each
wadefletch Feb 10, 2026
12a6cac
fix: add idempotency guard to shutdown handler
wadefletch Feb 10, 2026
9c5aee1
refactor: simplify setupShutdownHooks to match trapUnhandledErrors
wadefletch Feb 10, 2026
6330afe
refactor: clean up shutdown tests with it.each and signal helpers
wadefletch Feb 10, 2026
0a7dc65
Merge branch 'main' into wade/graceful-shutdown
wadefletch Feb 10, 2026
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
34 changes: 30 additions & 4 deletions docs/1.docs/50.plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export default definePlugin((nitro) => {

### Available hooks

- `"request", (event) => {}`
- `"error", (error, { event? }) => {}`
- `"response", (event, { body }) => {}`
- `"request", (event) => {}` - Called when a request is received. Available in all presets.
- `"error", (error, { event? }) => {}` - Called when an error is captured. Available in all presets.
- `"response", (response, event) => {}` - Called when a response is sent. Available in all presets.
- `"close", () => {}` - Called when the server receives a shutdown signal (`SIGTERM` or `SIGINT`). Only available in long-running server presets (Node.js, Bun, Deno). Not called in serverless or edge environments.

## Examples

Expand All @@ -76,7 +77,32 @@ export default definePlugin((nitro) => {

### Graceful shutdown

Server will gracefully shutdown and wait for any background pending tasks initiated by event.waitUntil
On long-running server presets (`node-server`, `node-cluster`, `bun`, `deno-server`), the `close` hook fires when the process receives `SIGTERM` or `SIGINT`, allowing plugins to run async cleanup before exit.

```ts
import { definePlugin } from "nitro";

export default definePlugin((nitro) => {
nitro.hooks.hook("close", async () => {
await flushTelemetry();
await db.close();
});
})
```

Serverless and edge runtimes (Cloudflare Workers, AWS Lambda, Vercel, Netlify, Deno Deploy) do not have a shutdown signal–the platform terminates the execution context without notice. The `close` hook will not fire in these environments.

For per-request cleanup that works across all presets, use the `"response"` hook or `request.waitUntil()` instead:

```ts
import { definePlugin } from "nitro";

export default definePlugin((nitro) => {
nitro.hooks.hook("response", async (response, event) => {
await flushRequestTelemetry(event);
});
})
```

### Request and response lifecycle

Expand Down
8 changes: 3 additions & 5 deletions docs/2.deploy/10.runtimes/1.node.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ You can customize server behavior using following environment variables:

- `NITRO_PORT` or `PORT` (defaults to `3000`)
- `NITRO_HOST` or `HOST`
- `NITRO_UNIX_SOCKET` - if provided (a path to the desired socket file) the service will be served over the provided UNIX socket.
- `NITRO_UNIX_SOCKET` - if provided (a path to the desired socket file) the service will be served over the provided UNIX socket (Node.js and Bun only).
- `NITRO_SSL_CERT` and `NITRO_SSL_KEY` - if both are present, this will launch the server in HTTPS mode. In the vast majority of cases, this should not be used other than for testing, and the Nitro server should be run behind a reverse proxy like nginx or Cloudflare which terminates SSL.
- `NITRO_SHUTDOWN_DISABLED` - Disables the graceful shutdown feature when set to `'true'`. If it's set to `'true'`, the graceful shutdown is bypassed to speed up the development process. Defaults to `'false'`.
- `NITRO_SHUTDOWN_SIGNALS` - Allows you to specify which signals should be handled. Each signal should be separated with a space. Defaults to `'SIGINT SIGTERM'`.
- `NITRO_SHUTDOWN_TIMEOUT` - Sets the amount of time (in milliseconds) before a forced shutdown occurs. Defaults to `'30000'` milliseconds.
- `NITRO_SHUTDOWN_FORCE` - When set to true, it triggers `process.exit()` at the end of the shutdown process. If it's set to `'false'`, the process will simply let the event loop clear. Defaults to `'true'`.
- `NITRO_SHUTDOWN_DISABLED` - Disables the graceful shutdown feature when set to `'true'`. Defaults to `'false'`.
- `NITRO_SHUTDOWN_TIMEOUT` - Sets the amount of time (in milliseconds) before a forced shutdown occurs. Defaults to `5000` milliseconds.

## Cluster mode

Expand Down
11 changes: 11 additions & 0 deletions docs/2.deploy/10.runtimes/bun.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,14 @@ bun run ./.output/server/index.mjs
```

:read-more{to="https://bun.sh"}

### Environment Variables
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Heading level skips from h1 to h3.

There's no h2 between # Bun (line 5) and ### Environment Variables (line 21). This should be ## to comply with Markdown heading hierarchy.

πŸ“ Suggested fix
-### Environment Variables
+## Environment Variables
πŸ€– Prompt for AI Agents
In `@docs/2.deploy/10.runtimes/bun.md` at line 21, The "### Environment Variables"
heading is one level too deep relative to the top-level "# Bun" header; change
the "### Environment Variables" heading text to "## Environment Variables" so
the document uses an h2 under "# Bun" and preserves Markdown heading hierarchy
(locate the string "### Environment Variables" in the file and replace it with
"## Environment Variables").


You can customize server behavior using following environment variables:

- `NITRO_PORT` or `PORT` (defaults to `3000`)
- `NITRO_HOST` or `HOST`
- `NITRO_UNIX_SOCKET` - if provided (a path to the desired socket file) the service will be served over the provided UNIX socket.
- `NITRO_SSL_CERT` and `NITRO_SSL_KEY` - if both are present, this will launch the server in HTTPS mode. In the vast majority of cases, this should not be used other than for testing, and the Nitro server should be run behind a reverse proxy like nginx or Cloudflare which terminates SSL.
- `NITRO_SHUTDOWN_DISABLED` - Disables the graceful shutdown feature when set to `'true'`. Defaults to `'false'`.
- `NITRO_SHUTDOWN_TIMEOUT` - Sets the amount of time (in milliseconds) before a forced shutdown occurs. Defaults to `5000` milliseconds.
10 changes: 10 additions & 0 deletions docs/2.deploy/10.runtimes/deno.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ NITRO_PRESET=deno_server npm run build
deno run --unstable --allow-net --allow-read --allow-env .output/server/index.ts
```

### Environment Variables
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Same heading-level skip as bun.md β€” should be ## not ###.

The file uses # Deno (h1) at line 5 and jumps to ### Environment Variables (h3) at line 21, skipping h2.

πŸ“ Suggested fix
-### Environment Variables
+## Environment Variables
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Environment Variables
## Environment Variables
🧰 Tools
πŸͺ› markdownlint-cli2 (0.20.0)

[warning] 21-21: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

πŸ€– Prompt for AI Agents
In `@docs/2.deploy/10.runtimes/deno.md` at line 21, The "Environment Variables"
heading currently uses h3 (###) but should be h2 (##) to follow the document's
heading hierarchy under the top-level "# Deno" heading; update the "Environment
Variables" heading to use "## Environment Variables" so it directly follows "#
Deno" and restores correct heading-level progression.


You can customize server behavior using following environment variables:

- `NITRO_PORT` or `PORT` (defaults to `3000`)
- `NITRO_HOST` or `HOST`
- `NITRO_SSL_CERT` and `NITRO_SSL_KEY` - if both are present, this will launch the server in HTTPS mode. In the vast majority of cases, this should not be used other than for testing, and the Nitro server should be run behind a reverse proxy like nginx or Cloudflare which terminates SSL.
- `NITRO_SHUTDOWN_DISABLED` - Disables the graceful shutdown feature when set to `'true'`. Defaults to `'false'`.
- `NITRO_SHUTDOWN_TIMEOUT` - Sets the amount of time (in milliseconds) before a forced shutdown occurs. Defaults to `5000` milliseconds.

## Deno Deploy

:read-more{to="/deploy/providers/deno-deploy"}
7 changes: 6 additions & 1 deletion src/presets/bun/runtime/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import wsAdapter from "crossws/adapters/bun";
import { useNitroApp } from "nitro/app";
import { startScheduleRunner } from "#nitro/runtime/task";
import { trapUnhandledErrors } from "#nitro/runtime/error/hooks";
import { resolveGracefulShutdownConfig, setupShutdownHooks } from "#nitro/runtime/shutdown";
import { resolveWebsocketHooks } from "#nitro/runtime/app";

const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? "");
const port = Number.isNaN(_parsedPort) ? 3000 : _parsedPort;
const host = process.env.NITRO_HOST || process.env.HOST;
const cert = process.env.NITRO_SSL_CERT;
const key = process.env.NITRO_SSL_KEY;
// const socketPath = process.env.NITRO_UNIX_SOCKET; // TODO
const socketPath = process.env.NITRO_UNIX_SOCKET;
const gracefulShutdown = resolveGracefulShutdownConfig();

const nitroApp = useNitroApp();

Expand All @@ -35,12 +37,15 @@ serve({
hostname: host,
tls: cert && key ? { cert, key } : undefined,
fetch: _fetch,
gracefulShutdown,
bun: {
unix: socketPath,
websocket: import.meta._websocket ? ws?.websocket : undefined,
},
});

trapUnhandledErrors();
setupShutdownHooks();

// Scheduled tasks
if (import.meta._tasks) {
Expand Down
5 changes: 4 additions & 1 deletion src/presets/deno/runtime/deno-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import wsAdapter from "crossws/adapters/deno";
import { useNitroApp } from "nitro/app";
import { startScheduleRunner } from "#nitro/runtime/task";
import { trapUnhandledErrors } from "#nitro/runtime/error/hooks";
import { resolveGracefulShutdownConfig, setupShutdownHooks } from "#nitro/runtime/shutdown";
import { resolveWebsocketHooks } from "#nitro/runtime/app";

const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? "");
Expand All @@ -14,7 +15,7 @@ const port = Number.isNaN(_parsedPort) ? 3000 : _parsedPort;
const host = process.env.NITRO_HOST || process.env.HOST;
const cert = process.env.NITRO_SSL_CERT;
const key = process.env.NITRO_SSL_KEY;
// const socketPath = process.env.NITRO_UNIX_SOCKET; // TODO
const gracefulShutdown = resolveGracefulShutdownConfig();

const nitroApp = useNitroApp();

Expand All @@ -35,9 +36,11 @@ serve({
hostname: host,
tls: cert && key ? { cert, key } : undefined,
fetch: _fetch,
gracefulShutdown,
});

trapUnhandledErrors();
setupShutdownHooks();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Scheduled tasks
if (import.meta._tasks) {
Expand Down
2 changes: 2 additions & 0 deletions src/presets/node/runtime/node-cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import wsAdapter from "crossws/adapters/node";
import { useNitroApp } from "nitro/app";
import { startScheduleRunner } from "#nitro/runtime/task";
import { trapUnhandledErrors } from "#nitro/runtime/error/hooks";
import { setupShutdownHooks } from "#nitro/runtime/shutdown";
import { resolveWebsocketHooks } from "#nitro/runtime/app";

const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? "");
Expand Down Expand Up @@ -46,6 +47,7 @@ if (import.meta._websocket) {
}

trapUnhandledErrors();
setupShutdownHooks();

// Scheduled tasks
if (import.meta._tasks) {
Expand Down
7 changes: 6 additions & 1 deletion src/presets/node/runtime/node-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import wsAdapter from "crossws/adapters/node";
import { useNitroApp } from "nitro/app";
import { startScheduleRunner } from "#nitro/runtime/task";
import { trapUnhandledErrors } from "#nitro/runtime/error/hooks";
import { resolveGracefulShutdownConfig, setupShutdownHooks } from "#nitro/runtime/shutdown";
import { resolveWebsocketHooks } from "#nitro/runtime/app";

const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? "");
Expand All @@ -13,7 +14,8 @@ const port = Number.isNaN(_parsedPort) ? 3000 : _parsedPort;
const host = process.env.NITRO_HOST || process.env.HOST;
const cert = process.env.NITRO_SSL_CERT;
const key = process.env.NITRO_SSL_KEY;
// const socketPath = process.env.NITRO_UNIX_SOCKET; // TODO
const socketPath = process.env.NITRO_UNIX_SOCKET;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could introduce this also in srvx directly via an env without NITRO_.

const gracefulShutdown = resolveGracefulShutdownConfig();

const nitroApp = useNitroApp();

Expand All @@ -22,6 +24,8 @@ const server = serve({
hostname: host,
tls: cert && key ? { cert, key } : undefined,
fetch: nitroApp.fetch,
gracefulShutdown,
node: socketPath ? { path: socketPath } : undefined,
});

if (import.meta._websocket) {
Expand All @@ -38,6 +42,7 @@ if (import.meta._websocket) {
}

trapUnhandledErrors();
setupShutdownHooks();

// Scheduled tasks
if (import.meta._tasks) {
Expand Down
32 changes: 32 additions & 0 deletions src/runtime/internal/shutdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useNitroApp } from "../app.ts";
import type { ServerOptions } from "srvx";

export function resolveGracefulShutdownConfig(): ServerOptions["gracefulShutdown"] {
if (process.env.NITRO_SHUTDOWN_DISABLED === "true") {
return false;
}
Comment thread
wadefletch marked this conversation as resolved.

const timeoutMs = Number.parseInt(process.env.NITRO_SHUTDOWN_TIMEOUT ?? "", 10);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is best that we don't introduce new NITRO_ env vars where srvx already supports SERVER_SHUTDOWN_TIMEOUT


if (timeoutMs > 0) {
// srvx expects timeout in seconds
return { gracefulTimeout: timeoutMs / 1000 };
}

return undefined;
}

async function _shutdownHandler() {
try {
await useNitroApp().hooks?.callHook("close");
} catch (error) {
console.error("[nitro] Error running close hook:", error);
}
}

export function setupShutdownHooks() {
if (typeof process !== "undefined" && process.on) {
process.on("SIGTERM", _shutdownHandler);
process.on("SIGINT", _shutdownHandler);
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Double signal delivery can invoke the close hook twice.

In production, it's common for orchestrators (Docker, Kubernetes, systemd) to send SIGTERM and then follow up with SIGINT if the process hasn't exited. Both signals would trigger _shutdownHandler, calling the close hook a second time while the first may still be in-flight. This can cause errors in plugin cleanup code (e.g., closing an already-closed database connection).

Consider guarding with a flag:

Proposed fix
+let _shutting = false;
+
 async function _shutdownHandler() {
+  if (_shutting) {
+    return;
+  }
+  _shutting = true;
   try {
     await useNitroApp().hooks?.callHook("close");
   } catch (error) {
     console.error("[nitro] Error running close hook:", error);
   }
 }
🧰 Tools
πŸͺ› Biome (2.3.13)

[error] 21-21: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

πŸ€– Prompt for AI Agents
In `@src/runtime/internal/shutdown.ts` around lines 19 - 31, _shutdownHandler can
be invoked twice by multiple signals; add an idempotency guard to prevent
reentrant calls: introduce a module-level boolean (e.g., isShuttingDown) checked
at the start of _shutdownHandler and set true before awaiting
useNitroApp().hooks?.callHook("close"), so subsequent SIGTERM/SIGINT handlers
return early; also remove or ignore the signal listeners in setupShutdownHooks
(or ensure process.on still attaches but relies on the boolean) so
hooks.callHook("close") only runs once and plugins aren't cleaned up twice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I declined to change this to follow the pattern from the pre-existing error handling hooks.

This is likely a good safety feature, though double-closes are in most cases no-op and/or can be handled in the application, depending on how abstracted/magic the hooks system should be–preferably less, imo, since hooks are by nature kind of a break through the abstraction wall.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wadefletch, thank you for the explanation. That makes senseβ€”keeping the hooks layer transparent and consistent with the existing error-handling patterns is a reasonable architectural choice. Application-level code can guard against double-close if needed, and avoiding extra magic in the hooks system preserves the "break through the abstraction wall" clarity you described. I appreciate the insight into the design philosophy.


✏️ Learnings added
Learnt from: wadefletch
Repo: nitrojs/nitro PR: 4017
File: src/runtime/internal/shutdown.ts:0-0
Timestamp: 2026-02-10T16:16:53.352Z
Learning: In Nitro's shutdown hooks (src/runtime/internal/shutdown.ts), double-signal delivery (e.g., SIGTERM followed by SIGINT) is intentionally not guarded with idempotency flags. The design follows pre-existing error handling hook patterns and prefers less abstraction/magic in the hooks system, allowing application-level code to handle double-close scenarios if needed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
133 changes: 133 additions & 0 deletions test/unit/shutdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const callHook = vi.fn().mockResolvedValue(undefined);

vi.mock("../../src/runtime/internal/app.ts", () => ({
useNitroApp: () => ({
hooks: { callHook },
}),
}));

import {
resolveGracefulShutdownConfig,
setupShutdownHooks,
} from "../../src/runtime/internal/shutdown.ts";

describe("resolveGracefulShutdownConfig", () => {
const env = process.env;

afterEach(() => {
process.env = env;
});

it("returns undefined by default", () => {
process.env = { ...env };
delete process.env.NITRO_SHUTDOWN_DISABLED;
delete process.env.NITRO_SHUTDOWN_TIMEOUT;
expect(resolveGracefulShutdownConfig()).toBeUndefined();
});

it.each([
{ value: "true", expected: false },
{ value: "false", expected: undefined },
{ value: "", expected: undefined },
{ value: "1", expected: undefined },
{ value: "yes", expected: undefined },
])("NITRO_SHUTDOWN_DISABLED=$value returns $expected", ({ value, expected }) => {
process.env = { ...env, NITRO_SHUTDOWN_DISABLED: value };
delete process.env.NITRO_SHUTDOWN_TIMEOUT;
expect(resolveGracefulShutdownConfig()).toBe(expected);
});

it("returns gracefulTimeout in seconds from NITRO_SHUTDOWN_TIMEOUT ms", () => {
process.env = { ...env, NITRO_SHUTDOWN_TIMEOUT: "10000" };
delete process.env.NITRO_SHUTDOWN_DISABLED;
expect(resolveGracefulShutdownConfig()).toEqual({ gracefulTimeout: 10 });
});

it("disabled takes priority over timeout", () => {
process.env = {
...env,
NITRO_SHUTDOWN_DISABLED: "true",
NITRO_SHUTDOWN_TIMEOUT: "10000",
};
expect(resolveGracefulShutdownConfig()).toBe(false);
});

it("ignores non-numeric timeout", () => {
process.env = { ...env, NITRO_SHUTDOWN_TIMEOUT: "abc" };
delete process.env.NITRO_SHUTDOWN_DISABLED;
expect(resolveGracefulShutdownConfig()).toBeUndefined();
});
});

describe("setupShutdownHooks", () => {
let savedSIGTERM: Function[];
let savedSIGINT: Function[];

beforeEach(() => {
savedSIGTERM = process.listeners("SIGTERM").slice();
savedSIGINT = process.listeners("SIGINT").slice();
callHook.mockClear();
callHook.mockResolvedValue(undefined);
});

afterEach(() => {
process.removeAllListeners("SIGTERM");
process.removeAllListeners("SIGINT");
for (const fn of savedSIGTERM) process.on("SIGTERM", fn as () => void);
for (const fn of savedSIGINT) process.on("SIGINT", fn as () => void);
});

it("registers SIGTERM and SIGINT handlers", () => {
const beforeTERM = process.listenerCount("SIGTERM");
const beforeINT = process.listenerCount("SIGINT");
setupShutdownHooks();
expect(process.listenerCount("SIGTERM")).toBe(beforeTERM + 1);
expect(process.listenerCount("SIGINT")).toBe(beforeINT + 1);
});

it("calls close hook on SIGTERM", async () => {
setupShutdownHooks();
process.emit("SIGTERM", "SIGTERM");
await vi.waitFor(() => {
expect(callHook).toHaveBeenCalledWith("close");
});
});

it("calls close hook on SIGINT", async () => {
setupShutdownHooks();
process.emit("SIGINT", "SIGINT");
await vi.waitFor(() => {
expect(callHook).toHaveBeenCalledWith("close");
});
});

it("awaits the close hook promise", async () => {
let resolved = false;
callHook.mockImplementation(
() =>
new Promise<void>((r) => {
setTimeout(() => {
resolved = true;
r();
}, 50);
})
);
setupShutdownHooks();
process.emit("SIGTERM", "SIGTERM");
await vi.waitFor(() => expect(resolved).toBe(true));
});

it("logs error if close hook throws", async () => {
const error = new Error("cleanup failed");
callHook.mockRejectedValueOnce(error);
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setupShutdownHooks();
process.emit("SIGTERM", "SIGTERM");
await vi.waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith("[nitro] Error running close hook:", error);
});
consoleSpy.mockRestore();
});
});
Loading