My aggregated monorepo of OCaml code, automaintained
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)