Allow storefront deploys to wait up to 30 minutes.

Node fetch was aborting at undici's 5-minute headers timeout, so Super Admin marked long npm ci / next builds as failed while the websites VM was still compiling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-04 15:22:23 +03:30
co-authored by Cursor
parent d14f91a606
commit 53fe8b72bd
2 changed files with 56 additions and 4 deletions
@@ -2,6 +2,43 @@ import { Injectable, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { provisionUrlFromDeployUrl, sslUrlFromDeployUrl, parkUrlFromDeployUrl, unparkUrlFromDeployUrl } from './website-deploy.util';
/** Cold `npm ci` + `next build` often exceeds 5–10 minutes. */
const DEPLOY_WAIT_MS = 30 * 60 * 1000;
/**
* Node `fetch` (undici) defaults `headersTimeout` to 5 minutes. AbortSignal
* does not override that — without this agent, Nest aborts while the websites
* VM is still compiling and Super Admin shows a false "Deploy failed".
* `node:undici` is built into Node 20; this project's @types/node has no types.
*/
const deployWaitAgent = new (
require('node:undici') as {
Agent: new (opts: {
headersTimeout?: number;
bodyTimeout?: number;
connectTimeout?: number;
}) => object;
}
).Agent({
headersTimeout: DEPLOY_WAIT_MS,
bodyTimeout: DEPLOY_WAIT_MS,
connectTimeout: 30_000,
});
function isFetchWaitTimeout(err: unknown): boolean {
if (!(err instanceof Error)) return false;
if (err.name === 'TimeoutError' || err.name === 'HeadersTimeoutError' || err.name === 'BodyTimeoutError') {
return true;
}
const cause = (err as { cause?: { code?: string; name?: string } }).cause;
return (
cause?.code === 'UND_ERR_HEADERS_TIMEOUT' ||
cause?.code === 'UND_ERR_BODY_TIMEOUT' ||
cause?.name === 'HeadersTimeoutError' ||
cause?.name === 'BodyTimeoutError'
);
}
@Injectable()
export class WebsiteDeployAgentService {
constructor(private readonly config: ConfigService) {}
@@ -60,12 +97,13 @@ export class WebsiteDeployAgentService {
'X-Deploy-Token': token,
},
body: JSON.stringify({ slug, wait }),
signal: AbortSignal.timeout(15 * 60 * 1000),
});
signal: AbortSignal.timeout(DEPLOY_WAIT_MS),
dispatcher: deployWaitAgent,
} as RequestInit);
} catch (err) {
if (err instanceof Error && err.name === 'TimeoutError') {
if (isFetchWaitTimeout(err)) {
throw new ServiceUnavailableException(
'Deploy timed out waiting for websites server (15m)',
'Deploy timed out waiting for websites server (30m)',
);
}
throw new ServiceUnavailableException('Could not reach website deploy agent');