Skip to content

Commit 1312d48

Browse files
V48 (impl-only): Surface Sandbox API 400 body and cap create timeout
Expand generic "Status code 400 is not ok" from @vercel/sandbox with response JSON/text so deposit synthesis failures show the real create reject reason. Emit image/hasSource on sandbox-create-started; clamp timeout to 45m.
1 parent e2b6f5f commit 1312d48

1 file changed

Lines changed: 80 additions & 7 deletions

File tree

packages/generic-hosts/VercelSandbox/src/vercel-sandbox-host.ts

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,22 @@ export class VercelSandboxPipelineHost {
5353
type: 'sandbox-create-started',
5454
timestamp: new Date().toISOString(),
5555
runtime: createOptions.runtime,
56+
// Source-safe: image ref only (no tokens). Helps diagnose create 400s.
57+
image: createOptions.image ?? null,
5658
mode: plan.manifest.hostMode,
57-
});
58-
const sandbox = await withTimeout(
59-
this.sandboxFactory.create(withVercelAccessTokenAuth(createOptions)),
60-
this.sandboxCreateTimeoutMs,
61-
`Vercel Sandbox create did not complete within ${this.sandboxCreateTimeoutMs}ms.`
62-
);
59+
hasSource: Boolean(createOptions.source),
60+
persistent: createOptions.persistent === true,
61+
} as PipelineHostEvent);
62+
let sandbox: SandboxSession;
63+
try {
64+
sandbox = await withTimeout(
65+
this.sandboxFactory.create(withVercelAccessTokenAuth(createOptions)),
66+
this.sandboxCreateTimeoutMs,
67+
`Vercel Sandbox create did not complete within ${this.sandboxCreateTimeoutMs}ms.`,
68+
);
69+
} catch (error) {
70+
throw new Error(formatSandboxApiError(error, 'Sandbox.create'), { cause: error });
71+
}
6372
const sandboxIdentity = resolveSandboxIdentity(sandbox, createOptions.name);
6473
await this.emit({
6574
type: 'sandbox-created',
@@ -453,15 +462,79 @@ export function normalizeCreateOptions(
453462
? createOptions.image.trim()
454463
: undefined;
455464
// Vercel SDK: runtime and image are mutually exclusive.
456-
const { runtime: _runtime, ...rest } = createOptions;
465+
const { runtime: _runtime, image: _image, ...rest } = createOptions;
466+
// Cap session timeout — excessive values can yield API 400 from Sandbox create.
467+
const rawTimeout =
468+
typeof createOptions.timeout === 'number' && Number.isFinite(createOptions.timeout)
469+
? createOptions.timeout
470+
: undefined;
471+
const timeout =
472+
typeof rawTimeout === 'number'
473+
? Math.min(Math.max(rawTimeout, 60_000), 45 * 60 * 1000)
474+
: undefined;
457475
return {
458476
...rest,
459477
persistent,
460478
name,
479+
...(typeof timeout === 'number' ? { timeout } : {}),
461480
...(image ? { image } : { runtime: createOptions.runtime }),
462481
};
463482
}
464483

484+
/**
485+
* Expand Vercel Sandbox SDK APIError ("Status code 400 is not ok") with response
486+
* body fields so Production logs / UI can show the real reject reason.
487+
*/
488+
export function formatSandboxApiError(error: unknown, phase: string): string {
489+
if (!(error instanceof Error)) {
490+
return `${phase} failed: ${String(error)}`;
491+
}
492+
const anyErr = error as Error & {
493+
json?: unknown;
494+
text?: string;
495+
response?: { status?: number; statusText?: string };
496+
sandboxName?: string;
497+
};
498+
const status = anyErr.response?.status;
499+
const statusText = anyErr.response?.statusText;
500+
let detail = '';
501+
if (anyErr.json && typeof anyErr.json === 'object') {
502+
const body = anyErr.json as Record<string, unknown>;
503+
const msg =
504+
(typeof body.message === 'string' && body.message) ||
505+
(typeof body.error === 'string' && body.error) ||
506+
(body.error &&
507+
typeof body.error === 'object' &&
508+
typeof (body.error as { message?: string }).message === 'string' &&
509+
(body.error as { message: string }).message) ||
510+
null;
511+
const code =
512+
(typeof body.code === 'string' && body.code) ||
513+
(body.error &&
514+
typeof body.error === 'object' &&
515+
typeof (body.error as { code?: string }).code === 'string' &&
516+
(body.error as { code: string }).code) ||
517+
null;
518+
detail = [code, msg].filter(Boolean).join(': ');
519+
if (!detail) {
520+
try {
521+
detail = JSON.stringify(body).slice(0, 400);
522+
} catch {
523+
detail = '';
524+
}
525+
}
526+
} else if (typeof anyErr.text === 'string' && anyErr.text.trim()) {
527+
detail = anyErr.text.trim().slice(0, 400);
528+
}
529+
const base = error.message?.trim() || 'unknown error';
530+
const statusPart =
531+
typeof status === 'number' ? `HTTP ${status}${statusText ? ` ${statusText}` : ''}` : null;
532+
const parts = [`${phase} failed`, statusPart, base !== `Status code ${status} is not ok` ? base : null, detail]
533+
.filter(Boolean)
534+
.filter((part, index, arr) => arr.indexOf(part) === index);
535+
return parts.join(' — ');
536+
}
537+
465538
function resolveSandboxIdentity(
466539
sandbox: SandboxSession,
467540
fallbackName?: string,

0 commit comments

Comments
 (0)