Skip to content
Open
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: 7 additions & 0 deletions src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const _devHint = `(is dev server running?)`;
async function _getTasksContext(opts?: TaskRunnerOptions) {
const cwd = resolve(process.cwd(), opts?.cwd || ".");
const buildDir = resolve(cwd, opts?.buildDir || "node_modules/.nitro");
const timeout = opts?.timeout ?? 30_000;

const buildInfoPath = resolve(buildDir, "nitro.dev.json");
if (!existsSync(buildInfoPath)) {
Expand Down Expand Up @@ -75,6 +76,7 @@ async function _getTasksContext(opts?: TaskRunnerOptions) {
{
socketPath,
method: options?.method,
timeout,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Expand All @@ -95,6 +97,11 @@ async function _getTasksContext(opts?: TaskRunnerOptions) {
}
);

request.on("timeout", () => {
// destroy with an error so the `error` handler rejects instead of
// leaving the promise pending forever on a stalled socket
request.destroy(new Error(`Request timed out after ${timeout}ms ${_devHint}`));
});
request.on("error", (e) => reject(e));

if (options?.body) {
Expand Down
6 changes: 6 additions & 0 deletions src/types/runtime/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ export interface Task<RT = unknown> {
export interface TaskRunnerOptions {
cwd?: string;
buildDir?: string;
/**
* Timeout in milliseconds for requests to the dev server.
*
* @default 30_000
*/
timeout?: number;
}
40 changes: 40 additions & 0 deletions test/unit/task.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import http from "node:http";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "pathe";
import { afterAll, describe, expect, it } from "vitest";
import { listTasks } from "../../src/task.ts";

describe("task runner devFetch", () => {
const cleanups: Array<() => Promise<void> | void> = [];

afterAll(async () => {
for (const cleanup of cleanups) {
await cleanup();
}
});

// https://github.com/unjs/nitro/issues/4292
it("rejects instead of hanging when the dev server socket stalls", async () => {
const cwd = await mkdtemp(join(tmpdir(), "nitro-task-test-"));
cleanups.push(() => rm(cwd, { recursive: true, force: true }));

// A worker socket that accepts connections but never responds, like a
// stalled dev server whose pid is still alive.
const socketPath = join(cwd, "worker.sock");
const server = http.createServer(() => {});
cleanups.push(() => new Promise((resolve) => server.close(() => resolve())));
await new Promise<void>((resolve) => server.listen(socketPath, resolve));

const buildDir = join(cwd, "node_modules/.nitro");
await mkdir(buildDir, { recursive: true });
await writeFile(
join(buildDir, "nitro.dev.json"),
JSON.stringify({
dev: { pid: process.pid, workerAddress: { socketPath } },
})
);

await expect(listTasks({ cwd, timeout: 200 })).rejects.toThrow(/timed out/i);
}, 5000);
});