@@ -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+
465538function resolveSandboxIdentity (
466539 sandbox : SandboxSession ,
467540 fallbackName ?: string ,
0 commit comments