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:
co-authored by
Cursor
parent
d14f91a606
commit
53fe8b72bd
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user