My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

at main 156 lines 5.1 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** NestJS-style API error handling. 7 8 NestJS/Express applications return errors in a standard format: 9 {[ 10 { 11 "message": "Missing required permission: person.read", 12 "error": "Forbidden", 13 "statusCode": 403, 14 "correlationId": "koskgk9d" 15 } 16 ]} 17 18 This module provides types and utilities for parsing and handling 19 these errors in a structured way. 20 21 {2 Usage} 22 23 {[ 24 match Immich.People.get_all_people client () with 25 | people -> ... 26 | exception Openapi.Runtime.Api_error e -> 27 match Openapi.Nestjs.of_api_error e with 28 | Some nestjs_error -> 29 Fmt.epr "Error: %s (correlation: %s)@." 30 nestjs_error.message 31 (Option.value ~default:"none" nestjs_error.correlation_id) 32 | None -> 33 (* Not a NestJS error, use raw body *) 34 Fmt.epr "Error: %s@." e.body 35 ]} 36*) 37 38(** {1 Error Types} *) 39 40(** A structured NestJS HTTP exception. *) 41type t = { 42 status_code : int; 43 (** HTTP status code (e.g., 403, 404, 500). *) 44 45 error : string option; 46 (** Error category (e.g., "Forbidden", "Not Found", "Internal Server Error"). *) 47 48 message : string; 49 (** Human-readable error message. Can be a single string or concatenated 50 from an array of validation messages. *) 51 52 correlation_id : string option; 53 (** Request correlation ID for debugging/support. *) 54} 55 56(** {1 JSON Codec} *) 57 58(** Jsont codec for NestJS errors. 59 60 Handles both string and array message formats: 61 - {[ "message": "error text" ]} 62 - {[ "message": ["validation error 1", "validation error 2"] ]} *) 63let jsont : t Jsont.t = 64 (* Message can be string or array of strings *) 65 let message_jsont = 66 Jsont.map Jsont.json ~kind:"message" 67 ~dec:(fun json -> 68 match json with 69 | Jsont.String (s, _) -> s 70 | Jsont.Array (items, _) -> 71 items 72 |> List.filter_map (function 73 | Jsont.String (s, _) -> Some s 74 | _ -> None) 75 |> String.concat "; " 76 | _ -> "Unknown error") 77 ~enc:(fun s -> Jsont.String (s, Jsont.Meta.none)) 78 in 79 Jsont.Object.map ~kind:"NestjsError" 80 (fun status_code error message correlation_id -> 81 { status_code; error; message; correlation_id }) 82 |> Jsont.Object.mem "statusCode" Jsont.int ~enc:(fun e -> e.status_code) 83 |> Jsont.Object.opt_mem "error" Jsont.string ~enc:(fun e -> e.error) 84 |> Jsont.Object.mem "message" message_jsont ~enc:(fun e -> e.message) 85 |> Jsont.Object.opt_mem "correlationId" Jsont.string ~enc:(fun e -> e.correlation_id) 86 |> Jsont.Object.skip_unknown 87 |> Jsont.Object.finish 88 89(** {1 Parsing} *) 90 91(** Parse a JSON string into a NestJS error. 92 Returns [None] if the string is not valid NestJS error JSON. *) 93let of_string (s : string) : t option = 94 match Jsont_bytesrw.decode_string jsont s with 95 | Ok e -> Some e 96 | Error _ -> None 97 98(** Parse an {!Openapi.Runtime.Api_error} into a structured NestJS error. 99 Returns [None] if the error body is not valid NestJS error JSON. *) 100let of_api_error (e : Openapi_runtime.api_error) : t option = 101 of_string e.body 102 103(** {1 Convenience Functions} *) 104 105(** Check if this is a permission/authorization error (401 or 403). *) 106let is_auth_error (e : t) : bool = 107 e.status_code = 401 || e.status_code = 403 108 109(** Check if this is a "not found" error (404). *) 110let is_not_found (e : t) : bool = 111 e.status_code = 404 112 113(** Check if this is a validation error (400 with message array). *) 114let is_validation_error (e : t) : bool = 115 e.status_code = 400 116 117(** Check if this is a server error (5xx). *) 118let is_server_error (e : t) : bool = 119 e.status_code >= 500 && e.status_code < 600 120 121(** {1 Pretty Printing} *) 122 123(** Pretty-print a NestJS error. *) 124let pp ppf (e : t) = 125 match e.correlation_id with 126 | Some cid -> 127 Format.fprintf ppf "%s [%d] (correlationId: %s)" 128 e.message e.status_code cid 129 | None -> 130 Format.fprintf ppf "%s [%d]" e.message e.status_code 131 132(** Convert to a human-readable string. *) 133let to_string (e : t) : string = 134 Format.asprintf "%a" pp e 135 136(** {1 Exception Handling} *) 137 138(** Exception for NestJS-specific errors. 139 Use this when you want to distinguish NestJS errors from generic API errors. *) 140exception Error of t 141 142(** Register a pretty printer for the exception. *) 143let () = 144 Printexc.register_printer (function 145 | Error e -> Some (Format.asprintf "Nestjs.Error: %a" pp e) 146 | _ -> None) 147 148(** Handle an {!Openapi.Runtime.Api_error}, converting it to a NestJS error 149 if possible. 150 151 @raise Error if the error body parses as a NestJS error 152 @raise Openapi.Runtime.Api_error if parsing fails (re-raises original) *) 153let raise_if_nestjs (e : Openapi_runtime.api_error) = 154 match of_api_error e with 155 | Some nestjs -> raise (Error nestjs) 156 | None -> raise (Openapi_runtime.Api_error e)