Use Node http for long storefront deploys instead of undici.
This API VM's Node 20 build has no node:undici, so require() crash-looped the process and took login down. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
53fe8b72bd
commit
5c701d0841
@@ -1,42 +1,62 @@
|
|||||||
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
import { provisionUrlFromDeployUrl, sslUrlFromDeployUrl, parkUrlFromDeployUrl, unparkUrlFromDeployUrl } from './website-deploy.util';
|
import { provisionUrlFromDeployUrl, sslUrlFromDeployUrl, parkUrlFromDeployUrl, unparkUrlFromDeployUrl } from './website-deploy.util';
|
||||||
|
|
||||||
/** Cold `npm ci` + `next build` often exceeds 5–10 minutes. */
|
/** Cold `npm ci` + `next build` often exceeds 5–10 minutes. */
|
||||||
const DEPLOY_WAIT_MS = 30 * 60 * 1000;
|
const DEPLOY_WAIT_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Node `fetch` (undici) defaults `headersTimeout` to 5 minutes. AbortSignal
|
* Long POST without `fetch`. This API VM's Node 20 build has no `node:undici`,
|
||||||
* does not override that — without this agent, Nest aborts while the websites
|
* and undici's default headersTimeout is 5 minutes even when it is present.
|
||||||
* 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 (
|
function postJson(
|
||||||
require('node:undici') as {
|
url: string,
|
||||||
Agent: new (opts: {
|
headers: Record<string, string>,
|
||||||
headersTimeout?: number;
|
bodyObj: unknown,
|
||||||
bodyTimeout?: number;
|
timeoutMs: number,
|
||||||
connectTimeout?: number;
|
): Promise<{ ok: boolean; status: number; text: string }> {
|
||||||
}) => object;
|
return new Promise((resolve, reject) => {
|
||||||
}
|
const body = JSON.stringify(bodyObj);
|
||||||
).Agent({
|
const parsed = new URL(url);
|
||||||
headersTimeout: DEPLOY_WAIT_MS,
|
const lib = parsed.protocol === 'https:' ? https : http;
|
||||||
bodyTimeout: DEPLOY_WAIT_MS,
|
const req = lib.request(
|
||||||
connectTimeout: 30_000,
|
{
|
||||||
});
|
protocol: parsed.protocol,
|
||||||
|
hostname: parsed.hostname,
|
||||||
function isFetchWaitTimeout(err: unknown): boolean {
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
if (!(err instanceof Error)) return false;
|
path: `${parsed.pathname}${parsed.search}`,
|
||||||
if (err.name === 'TimeoutError' || err.name === 'HeadersTimeoutError' || err.name === 'BodyTimeoutError') {
|
method: 'POST',
|
||||||
return true;
|
headers: {
|
||||||
}
|
...headers,
|
||||||
const cause = (err as { cause?: { code?: string; name?: string } }).cause;
|
'Content-Type': 'application/json',
|
||||||
return (
|
'Content-Length': String(Buffer.byteLength(body)),
|
||||||
cause?.code === 'UND_ERR_HEADERS_TIMEOUT' ||
|
},
|
||||||
cause?.code === 'UND_ERR_BODY_TIMEOUT' ||
|
},
|
||||||
cause?.name === 'HeadersTimeoutError' ||
|
(res) => {
|
||||||
cause?.name === 'BodyTimeoutError'
|
const chunks: Buffer[] = [];
|
||||||
);
|
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
const status = res.statusCode ?? 0;
|
||||||
|
resolve({
|
||||||
|
ok: status >= 200 && status < 300,
|
||||||
|
status,
|
||||||
|
text: Buffer.concat(chunks).toString('utf8'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.setTimeout(timeoutMs, () => {
|
||||||
|
req.destroy();
|
||||||
|
const err = new Error('TimeoutError');
|
||||||
|
err.name = 'TimeoutError';
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -88,20 +108,16 @@ export class WebsiteDeployAgentService {
|
|||||||
const { deployUrl, token } = this.credentials();
|
const { deployUrl, token } = this.credentials();
|
||||||
const wait = options?.wait !== false;
|
const wait = options?.wait !== false;
|
||||||
|
|
||||||
let response: Response;
|
let result: { ok: boolean; status: number; text: string };
|
||||||
try {
|
try {
|
||||||
response = await fetch(deployUrl, {
|
result = await postJson(
|
||||||
method: 'POST',
|
deployUrl,
|
||||||
headers: {
|
{ 'X-Deploy-Token': token },
|
||||||
'Content-Type': 'application/json',
|
{ slug, wait },
|
||||||
'X-Deploy-Token': token,
|
DEPLOY_WAIT_MS,
|
||||||
},
|
);
|
||||||
body: JSON.stringify({ slug, wait }),
|
|
||||||
signal: AbortSignal.timeout(DEPLOY_WAIT_MS),
|
|
||||||
dispatcher: deployWaitAgent,
|
|
||||||
} as RequestInit);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isFetchWaitTimeout(err)) {
|
if (err instanceof Error && err.name === 'TimeoutError') {
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
'Deploy timed out waiting for websites server (30m)',
|
'Deploy timed out waiting for websites server (30m)',
|
||||||
);
|
);
|
||||||
@@ -109,7 +125,6 @@ export class WebsiteDeployAgentService {
|
|||||||
throw new ServiceUnavailableException('Could not reach website deploy agent');
|
throw new ServiceUnavailableException('Could not reach website deploy agent');
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await response.text().catch(() => '');
|
|
||||||
let payload: {
|
let payload: {
|
||||||
status?: string;
|
status?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
@@ -117,15 +132,15 @@ export class WebsiteDeployAgentService {
|
|||||||
error?: string;
|
error?: string;
|
||||||
} = {};
|
} = {};
|
||||||
try {
|
try {
|
||||||
payload = text ? (JSON.parse(text) as typeof payload) : {};
|
payload = result.text ? (JSON.parse(result.text) as typeof payload) : {};
|
||||||
} catch {
|
} catch {
|
||||||
/* keep raw */
|
/* keep raw */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!result.ok) {
|
||||||
const detail = payload.detail || payload.error || text;
|
const detail = payload.detail || payload.error || result.text;
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
`Deploy failed (${response.status})${detail ? `: ${detail}` : ''}`,
|
`Deploy failed (${result.status})${detail ? `: ${detail}` : ''}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user