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
@@ -153,6 +153,19 @@ server {
|
|||||||
|
|
||||||
client_max_body_size 15M;
|
client_max_body_size 15M;
|
||||||
|
|
||||||
|
# Storefront deploy (npm ci + next build) often exceeds 5 minutes
|
||||||
|
location ~ ^/api/v1/domains/[^/]+/deploy$ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 1800s;
|
||||||
|
proxy_read_timeout 1800s;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -160,6 +173,7 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,6 +2,43 @@ import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
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. */
|
||||||
|
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()
|
@Injectable()
|
||||||
export class WebsiteDeployAgentService {
|
export class WebsiteDeployAgentService {
|
||||||
constructor(private readonly config: ConfigService) {}
|
constructor(private readonly config: ConfigService) {}
|
||||||
@@ -60,12 +97,13 @@ export class WebsiteDeployAgentService {
|
|||||||
'X-Deploy-Token': token,
|
'X-Deploy-Token': token,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ slug, wait }),
|
body: JSON.stringify({ slug, wait }),
|
||||||
signal: AbortSignal.timeout(15 * 60 * 1000),
|
signal: AbortSignal.timeout(DEPLOY_WAIT_MS),
|
||||||
});
|
dispatcher: deployWaitAgent,
|
||||||
|
} as RequestInit);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.name === 'TimeoutError') {
|
if (isFetchWaitTimeout(err)) {
|
||||||
throw new ServiceUnavailableException(
|
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');
|
throw new ServiceUnavailableException('Could not reach website deploy agent');
|
||||||
|
|||||||
Reference in New Issue
Block a user