Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

at lex 372 lines 10 kB view raw
1import { z } from "zod"; 2import { 3 httpResponseCodeToName, 4 httpResponseCodeToString, 5 ResponseType, 6 ResponseTypeStrings, 7 XRPCError as XRPCClientError, 8} from "@atp/xrpc"; 9 10// @NOTE Do not depend (directly or indirectly) on "./types" here, as it would 11// create a circular dependency. 12 13/** 14 * Zod schema for error result objects. 15 * Defines the structure of error responses with status code and optional error/message fields. 16 */ 17export const errorResult: z.ZodObject<{ 18 status: z.ZodNumber; 19 error: z.ZodOptional<z.ZodString>; 20 message: z.ZodOptional<z.ZodString>; 21}> = z.object({ 22 status: z.number(), 23 error: z.string().optional(), 24 message: z.string().optional(), 25}); 26 27/** 28 * Type representing an error result object. 29 * Contains HTTP status code and optional error identifier and message. 30 */ 31export type ErrorResult = z.infer<typeof errorResult>; 32 33/** 34 * Type guard to check if a value is an ErrorResult. 35 * @param v - The value to check 36 * @returns True if the value matches the ErrorResult schema 37 */ 38export function isErrorResult(v: unknown): v is ErrorResult { 39 return errorResult.safeParse(v).success; 40} 41 42/** 43 * Type guard to check if a value is an HTTP Error-like object. 44 */ 45function isHttpErrorLike( 46 value: unknown, 47): value is { status: number; message: string; name: string } { 48 return ( 49 typeof value === "object" && 50 value !== null && 51 "status" in value && 52 "message" in value && 53 "name" in value && 54 typeof (value as Record<string, unknown>).status === "number" && 55 typeof (value as Record<string, unknown>).message === "string" && 56 typeof (value as Record<string, unknown>).name === "string" 57 ); 58} 59 60/** 61 * Excludes ErrorResult from a value type and throws if the value is an ErrorResult. 62 * @template V - The value type 63 * @param v - The value to check and exclude 64 * @returns The value if it's not an ErrorResult 65 * @throws {XRPCError} If the value is an ErrorResult 66 */ 67export function excludeErrorResult<V>(v: V): Exclude<V, ErrorResult> { 68 if (isErrorResult(v)) throw XRPCError.fromErrorResult(v); 69 return v as Exclude<V, ErrorResult>; 70} 71 72export { ResponseType }; 73 74/** 75 * Base class for all XRPC errors. 76 * Extends the standard Error class with XRPC-specific properties and methods. 77 */ 78export class XRPCError extends Error { 79 constructor( 80 public type: ResponseType, 81 public errorMessage?: string, 82 public customErrorName?: string, 83 options?: ErrorOptions, 84 ) { 85 super(errorMessage, options); 86 } 87 88 get statusCode(): number { 89 const { type } = this; 90 91 if (type < 400 || type >= 600 || !Number.isFinite(type)) { 92 return 500; 93 } 94 95 return type; 96 } 97 98 /** 99 * Gets the error payload for HTTP responses. 100 * For internal server errors (500), returns generic message instead of error details. 101 * @returns Object containing error name and message for the response 102 */ 103 get payload(): { 104 error: string | undefined; 105 message: string | undefined; 106 } { 107 return { 108 error: this.customErrorName ?? this.typeName, 109 message: this.type === ResponseType.InternalServerError 110 ? this.typeStr // Do not respond with error details for 500s 111 : this.errorMessage || this.typeStr, 112 }; 113 } 114 115 get typeName(): string | undefined { 116 return ResponseType[this.type]; 117 } 118 119 get typeStr(): string | undefined { 120 return ResponseTypeStrings[this.type]; 121 } 122 123 static fromError(cause: unknown): XRPCError { 124 if (cause instanceof XRPCError) { 125 return cause; 126 } 127 128 if (cause instanceof XRPCClientError) { 129 const { error, message, type } = mapFromClientError(cause); 130 return new XRPCError(type, message, error, { cause }); 131 } 132 133 if (isHttpErrorLike(cause)) { 134 return new XRPCError(cause.status, cause.message, cause.name, { cause }); 135 } 136 137 if (isErrorResult(cause)) { 138 return this.fromErrorResult(cause); 139 } 140 141 if (cause instanceof Error) { 142 return new InternalServerError(cause.message, undefined, { cause }); 143 } 144 145 return new InternalServerError( 146 "Unexpected internal server error", 147 undefined, 148 { cause }, 149 ); 150 } 151 152 static fromErrorResult(err: ErrorResult): XRPCError { 153 return new XRPCError(err.status, err.message, err.error, { cause: err }); 154 } 155} 156 157/** 158 * Error class for invalid request errors (HTTP 400). 159 * Used when the client request is malformed or invalid. 160 */ 161export class InvalidRequestError extends XRPCError { 162 constructor( 163 errorMessage?: string, 164 customErrorName?: string, 165 options?: ErrorOptions, 166 ) { 167 super(ResponseType.InvalidRequest, errorMessage, customErrorName, options); 168 } 169 170 [Symbol.hasInstance](instance: unknown): boolean { 171 return ( 172 instance instanceof XRPCError && 173 instance.type === ResponseType.InvalidRequest 174 ); 175 } 176} 177 178/** 179 * Error class for authentication required errors (HTTP 401). 180 * Used when the request requires authentication but none was provided or it was invalid. 181 */ 182export class AuthRequiredError extends XRPCError { 183 constructor( 184 errorMessage?: string, 185 customErrorName?: string, 186 options?: ErrorOptions, 187 ) { 188 super( 189 ResponseType.AuthenticationRequired, 190 errorMessage, 191 customErrorName, 192 options, 193 ); 194 } 195 196 [Symbol.hasInstance](instance: unknown): boolean { 197 return ( 198 instance instanceof XRPCError && 199 instance.type === ResponseType.AuthenticationRequired 200 ); 201 } 202} 203 204/** 205 * Error class for forbidden errors (HTTP 403). 206 * Used when the client is authenticated but doesn't have permission to access the resource. 207 */ 208export class ForbiddenError extends XRPCError { 209 constructor( 210 errorMessage?: string, 211 customErrorName?: string, 212 options?: ErrorOptions, 213 ) { 214 super(ResponseType.Forbidden, errorMessage, customErrorName, options); 215 } 216 217 [Symbol.hasInstance](instance: unknown): boolean { 218 return ( 219 instance instanceof XRPCError && instance.type === ResponseType.Forbidden 220 ); 221 } 222} 223 224/** 225 * Error class for internal server errors (HTTP 500). 226 * Used when an unexpected error occurs on the server side. 227 */ 228export class InternalServerError extends XRPCError { 229 constructor( 230 errorMessage?: string, 231 customErrorName?: string, 232 options?: ErrorOptions, 233 ) { 234 super( 235 ResponseType.InternalServerError, 236 errorMessage, 237 customErrorName, 238 options, 239 ); 240 } 241 242 [Symbol.hasInstance](instance: unknown): boolean { 243 return ( 244 instance instanceof XRPCError && 245 instance.type === ResponseType.InternalServerError 246 ); 247 } 248} 249 250/** 251 * Error class for upstream failure errors (HTTP 502). 252 * Used when a dependent service fails or returns an invalid response. 253 */ 254export class UpstreamFailureError extends XRPCError { 255 constructor( 256 errorMessage?: string, 257 customErrorName?: string, 258 options?: ErrorOptions, 259 ) { 260 super(ResponseType.UpstreamFailure, errorMessage, customErrorName, options); 261 } 262 263 [Symbol.hasInstance](instance: unknown): boolean { 264 return ( 265 instance instanceof XRPCError && 266 instance.type === ResponseType.UpstreamFailure 267 ); 268 } 269} 270 271/** 272 * Error class for not enough resources errors (HTTP 507). 273 * Used when the server temporarily cannot handle the request due to resource constraints. 274 */ 275export class NotEnoughResourcesError extends XRPCError { 276 constructor( 277 errorMessage?: string, 278 customErrorName?: string, 279 options?: ErrorOptions, 280 ) { 281 super( 282 ResponseType.NotEnoughResources, 283 errorMessage, 284 customErrorName, 285 options, 286 ); 287 } 288 289 [Symbol.hasInstance](instance: unknown): boolean { 290 return ( 291 instance instanceof XRPCError && 292 instance.type === ResponseType.NotEnoughResources 293 ); 294 } 295} 296 297/** 298 * Error class for upstream timeout errors (HTTP 504). 299 * Used when a dependent service times out or takes too long to respond. 300 */ 301export class UpstreamTimeoutError extends XRPCError { 302 constructor( 303 errorMessage?: string, 304 customErrorName?: string, 305 options?: ErrorOptions, 306 ) { 307 super(ResponseType.UpstreamTimeout, errorMessage, customErrorName, options); 308 } 309 310 [Symbol.hasInstance](instance: unknown): boolean { 311 return ( 312 instance instanceof XRPCError && 313 instance.type === ResponseType.UpstreamTimeout 314 ); 315 } 316} 317 318/** 319 * Error class for method not implemented errors (HTTP 501). 320 * Used when the requested XRPC method is not implemented by the server. 321 */ 322export class MethodNotImplementedError extends XRPCError { 323 constructor( 324 errorMessage?: string, 325 customErrorName?: string, 326 options?: ErrorOptions, 327 ) { 328 super( 329 ResponseType.MethodNotImplemented, 330 errorMessage, 331 customErrorName, 332 options, 333 ); 334 } 335 336 [Symbol.hasInstance](instance: unknown): boolean { 337 return ( 338 instance instanceof XRPCError && 339 instance.type === ResponseType.MethodNotImplemented 340 ); 341 } 342} 343 344function mapFromClientError(error: XRPCClientError): { 345 error: string; 346 message: string; 347 type: ResponseType; 348} { 349 switch (error.status) { 350 case ResponseType.InvalidResponse: 351 // Upstream server returned an XRPC response that is not compatible with our internal lexicon definitions for that XRPC method. 352 // @NOTE This could be reflected as both a 500 ("we" are at fault) and 502 ("they" are at fault). Let's be gents about it. 353 return { 354 error: httpResponseCodeToName(ResponseType.InternalServerError), 355 message: httpResponseCodeToString(ResponseType.InternalServerError), 356 type: ResponseType.InternalServerError, 357 }; 358 case ResponseType.Unknown: 359 // Typically a network error / unknown host 360 return { 361 error: httpResponseCodeToName(ResponseType.InternalServerError), 362 message: httpResponseCodeToString(ResponseType.InternalServerError), 363 type: ResponseType.InternalServerError, 364 }; 365 default: 366 return { 367 error: error.error, 368 message: error.message, 369 type: error.status, 370 }; 371 } 372}