@@ -20,6 +20,7 @@ import {
2020 EXECUTABLE_NAME ,
2121} from "./constants.js" ;
2222import { deleteStoredRefreshToken } from "./auth/refresh-token.js" ;
23+ import { describeHttpFailure } from "./errors.js" ;
2324
2425class Client {
2526 private endpoint : string ;
@@ -144,7 +145,8 @@ class Client {
144145 throw new AppwriteException ( "Invalid endpoint URL: " + endpoint ) ;
145146 }
146147
147- this . endpoint = endpoint ;
148+ // Paths are appended verbatim, so a trailing slash would produce `//account`.
149+ this . endpoint = endpoint . replace ( / \/ + $ / , "" ) ;
148150 return this ;
149151 }
150152
@@ -169,6 +171,15 @@ class Client {
169171 return { ...this . headers } ;
170172 }
171173
174+ /**
175+ * Records the endpoint the failed request used, so error hints can talk about
176+ * the endpoint actually called rather than the one in config.
177+ */
178+ private tagEndpoint < T extends Error > ( exception : T ) : T {
179+ ( exception as T & { endpoint ?: string } ) . endpoint = this . endpoint ;
180+ return exception ;
181+ }
182+
172183 async call < T = unknown > (
173184 method : string ,
174185 path : string = "" ,
@@ -227,17 +238,41 @@ class Client {
227238 } ) ,
228239 } ) ;
229240 } catch ( error ) {
230- throw new AppwriteException ( ( error as Error ) . message ) ;
241+ throw this . tagEndpoint ( new AppwriteException ( ( error as Error ) . message ) ) ;
231242 }
232243
233244 if ( response . status >= 400 ) {
234245 const text = await response . text ( ) ;
235246 let json : { message ?: string ; code ?: number ; type ?: string } | undefined =
236247 undefined ;
237248 try {
238- json = JSON . parse ( text ) ;
249+ const parsed : unknown = JSON . parse ( text ) ;
250+ if ( parsed && typeof parsed === "object" ) {
251+ json = parsed as { message ?: string ; code ?: number ; type ?: string } ;
252+ }
239253 } catch ( _error ) {
240- throw new AppwriteException ( text , response . status , "" , text ) ;
254+ json = undefined ;
255+ }
256+
257+ if ( ! json ) {
258+ // Proxies, load balancers and the console answer with HTML or plain
259+ // text. Summarize it — the raw body stays on the exception for
260+ // `--verbose` and `--report`.
261+ const failure = describeHttpFailure (
262+ response . status ,
263+ text ,
264+ response . statusText ,
265+ response . headers . get ( "content-type" ) ?? undefined ,
266+ ) ;
267+
268+ throw this . tagEndpoint (
269+ new AppwriteException (
270+ failure . message ,
271+ response . status ,
272+ failure . type ,
273+ text ,
274+ ) ,
275+ ) ;
241276 }
242277
243278 if (
@@ -262,19 +297,25 @@ class Client {
262297 / r o l e : \s * g u e s t s / i. test ( json . message ) ;
263298
264299 if ( isUnauthorized ) {
265- throw new AppwriteException (
266- `You are not authenticated. Run '${ EXECUTABLE_NAME } login' to authenticate and try again.` ,
267- json . code ,
268- json . type ,
269- text ,
300+ throw this . tagEndpoint (
301+ new AppwriteException (
302+ `You are not authenticated. Run '${ EXECUTABLE_NAME } login' to authenticate and try again.` ,
303+ json . code ,
304+ json . type ,
305+ text ,
306+ ) ,
270307 ) ;
271308 }
272309
273- throw new AppwriteException (
274- json . message || text ,
275- json . code ,
276- json . type ,
277- text ,
310+ throw this . tagEndpoint (
311+ new AppwriteException (
312+ json . message ||
313+ describeHttpFailure ( response . status , text , response . statusText )
314+ . message ,
315+ json . code ?? response . status ,
316+ json . type ,
317+ text ,
318+ ) ,
278319 ) ;
279320 }
280321
0 commit comments