@@ -38,6 +38,48 @@ function withEnvAuth(headers: Record<string, string>): Record<string, string> {
3838 return token ? { ...headers , Authorization : `Bearer ${ token } ` } : headers ;
3939}
4040
41+ /**
42+ * Read a code + human message out of a failed cloud response, whichever dialect
43+ * it arrived in.
44+ *
45+ * The routes behind this file (`service-cloud`) answer failures in two shapes,
46+ * and are mid-conversion from one to the other (cloud#944):
47+ *
48+ * { success: false, error: '<message>' } today, via `fail()`
49+ * { success: false, error: { code, message } } the declared envelope
50+ *
51+ * Seven call sites in this file each hand-rolled their own read of that, in
52+ * four different ways, and the spread was not harmless:
53+ *
54+ * - two read `payload?.error` bare, with no nested fallback. Once the
55+ * producer converts, those receive an OBJECT where the surrounding type
56+ * declares `error?: string` — and an object handed to a toast as a React
57+ * child crashes the page. That is objectstack#3913's exact symptom, which
58+ * `interpretActionResponse` already exists to prevent on the actions route.
59+ * - two read only the nested form, so against a server shipping TODAY they
60+ * find nothing and degrade a real explanation into a bare status code.
61+ *
62+ * So this is not only preparation for the conversion — it fixes messages that
63+ * are already being swallowed. One function, so the next reader has one place
64+ * to look and the next route has nothing to hand-roll.
65+ *
66+ * The `typeof … === 'string'` guard is what makes it safe to hand the result
67+ * straight to a `string` field: an unexpected shape degrades to the code rather
68+ * than travelling onward as an object.
69+ */
70+ export function readApiError (
71+ payload : any ,
72+ res : { status : number ; statusText : string } ,
73+ ) : { code : string ; message : string } {
74+ const code = payload ?. error ?. code ?? payload ?. code ?? `HTTP_${ res . status } ` ;
75+ const raw =
76+ payload ?. error ?. message // the declared envelope
77+ ?? payload ?. error // `fail()`'s bare string
78+ ?? payload ?. message // a few routes put the text here
79+ ?? res . statusText ;
80+ return { code : String ( code ) , message : typeof raw === 'string' ? raw : String ( code ) } ;
81+ }
82+
4183/**
4284 * Per-locale overrides for translatable package fields. Mirrors
4385 * `PackageTranslation` from @objectstack/spec/cloud — duplicated here
@@ -143,8 +185,7 @@ async function call<T>(path: string, init?: RequestInit): Promise<T> {
143185 let payload : any = null ;
144186 try { payload = await res . json ( ) ; } catch { /* empty body */ }
145187 if ( ! res . ok ) {
146- const code = payload ?. error ?. code ?? payload ?. code ?? `HTTP_${ res . status } ` ;
147- const message = payload ?. error ?. message ?? payload ?. error ?? res . statusText ;
188+ const { code, message } = readApiError ( payload , res ) ;
148189 const err = new Error ( typeof message === 'string' ? message : `${ code } ` ) ;
149190 ( err as any ) . code = code ;
150191 ( err as any ) . status = res . status ;
@@ -221,8 +262,7 @@ export async function installPackage(input: {
221262 let payload : any = null ;
222263 try { payload = await res . json ( ) ; } catch { /* empty */ }
223264 if ( ! res . ok || payload ?. success === false ) {
224- const code = payload ?. error ?. code ?? payload ?. code ?? `HTTP_${ res . status } ` ;
225- const message = payload ?. error ?. message ?? payload ?. error ?? payload ?. message ?? res . statusText ;
265+ const { code, message } = readApiError ( payload , res ) ;
226266 const err = new Error ( typeof message === 'string' ? message : `${ code } ` ) ;
227267 ( err as any ) . code = code ;
228268 ( err as any ) . status = res . status ;
@@ -255,13 +295,11 @@ export async function installPackage(input: {
255295 const inner = payload ?. data ;
256296 const innerFailed = ! ! inner && typeof inner === 'object' && inner . success === false ;
257297 if ( ! res . ok || innerFailed ) {
258- const code = payload ?. code ?? payload ?. error ?. code ?? `HTTP_${ res . status } ` ;
259- // `error` is a nested `{code, message}` object on the current wire and a
260- // plain string on the older one — read the message out of both before
261- // falling back, or a real explanation degrades into a bare status code.
262- const message = innerFailed
263- ? ( inner . error ?? res . statusText )
264- : ( payload ?. error ?. message ?? payload ?. error ?? payload ?. message ?? res . statusText ) ;
298+ // The inner branch reads a DIFFERENT object — the 200-with-`data.success:
299+ // false` shape above — so it stays hand-read; only the outer one is shared.
300+ const outer = readApiError ( payload , res ) ;
301+ const code = payload ?. code ?? outer . code ;
302+ const message = innerFailed ? ( inner . error ?? res . statusText ) : outer . message ;
265303 const err = new Error ( typeof message === 'string' ? message : `${ code } ` ) ;
266304 ( err as any ) . code = code ;
267305 ( err as any ) . status = res . status ;
@@ -488,7 +526,7 @@ export async function reseedSampleData(installationId: string): Promise<{ ok: bo
488526 } ) ;
489527 const payload : any = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
490528 if ( ! res . ok || payload ?. success === false ) {
491- return { ok : false , error : payload ?. error || `HTTP ${ res . status } ` } ;
529+ return { ok : false , error : readApiError ( payload , res ) . message } ;
492530 }
493531 return { ok : true } ;
494532 } catch ( err : any ) {
@@ -509,7 +547,7 @@ export async function purgeSampleData(installationId: string): Promise<{ ok: boo
509547 } ) ;
510548 const payload : any = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
511549 if ( ! res . ok || payload ?. success === false ) {
512- return { ok : false , error : payload ?. error || `HTTP ${ res . status } ` } ;
550+ return { ok : false , error : readApiError ( payload , res ) . message } ;
513551 }
514552 return { ok : true , deleted : Number ( payload ?. data ?. deleted ?? 0 ) } ;
515553 } catch ( err : any ) {
@@ -604,8 +642,7 @@ export async function installLocal(input: {
604642 let payload : any = null ;
605643 try { payload = await res . json ( ) ; } catch { /* empty */ }
606644 if ( ! res . ok ) {
607- const code = payload ?. error ?. code ?? `HTTP_${ res . status } ` ;
608- const message = payload ?. error ?. message ?? res . statusText ;
645+ const { code, message } = readApiError ( payload , res ) ;
609646 const err = new Error ( typeof message === 'string' ? message : `${ code } ` ) ;
610647 ( err as any ) . code = code ;
611648 ( err as any ) . status = res . status ;
@@ -683,8 +720,7 @@ export async function uninstallLocal(manifestId: string): Promise<void> {
683720 } ) ;
684721 if ( ! res . ok ) {
685722 const payload : any = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
686- const message = payload ?. error ?. message ?? res . statusText ;
687- throw new Error ( typeof message === 'string' ? message : `HTTP_${ res . status } ` ) ;
723+ throw new Error ( readApiError ( payload , res ) . message ) ;
688724 }
689725}
690726
0 commit comments