Pure OCaml xxhash implementation

Add NestJS error handling module for structured API errors

- Add Openapi.Nestjs module for parsing NestJS/Express error responses
- Add Immich_auth.Error module with convenience wrappers
- Update immich CLI to display friendly error messages with:
- Error type (Forbidden, Not Found, etc.)
- Human-readable message
- HTTP status code
- Correlation ID for debugging
- Return appropriate exit codes (77 for auth errors, 69 for not found)

Example output:
Error: Forbidden: Missing required permission: person.read [403] (correlationId: abc123)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+565 -75
+89 -72
ocaml-immich/bin/cmd_faces.ml
··· 8 8 (* Search for people by name *) 9 9 10 10 let search_action ~requests_config ~profile ~name ~with_hidden env = 11 - Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client -> 12 - let api = Immich_auth.Client.client client in 13 - let session = Immich.session api in 14 - let base_url = Immich.base_url api in 15 - (* Person.search_person returns ResponseDto (single) but should be a list *) 16 - (* Use low-level API to get proper list response *) 17 - let query = Printf.sprintf "?name=%s%s" 18 - (Uri.pct_encode name) 19 - (if with_hidden then "&withHidden=true" else "") in 20 - let url = base_url ^ "/people/search" ^ query in 21 - let response = Requests.get session url in 22 - if Requests.Response.ok response then begin 23 - let json = Requests.Response.json response in 24 - let people = Openapi.Runtime.Json.decode_json_exn 25 - (Jsont.list Immich.Person.ResponseDto.jsont) json in 26 - if people = [] then 27 - Fmt.pr "No people found matching '%s'.@." name 28 - else begin 29 - Fmt.pr "People matching '%s':@." name; 30 - List.iter (fun person -> 31 - let name = Immich.Person.ResponseDto.name person in 32 - let id = Immich.Person.ResponseDto.id person in 33 - let hidden = if Immich.Person.ResponseDto.is_hidden person then " (hidden)" else "" in 34 - let display_name = if name = "" then "<unnamed>" else name in 35 - Fmt.pr " %s - %s%s@." id display_name hidden 36 - ) people 11 + Immich_auth.Error.wrap (fun () -> 12 + Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client -> 13 + let api = Immich_auth.Client.client client in 14 + let session = Immich.session api in 15 + let base_url = Immich.base_url api in 16 + (* Person.search_person returns ResponseDto (single) but should be a list *) 17 + (* Use low-level API to get proper list response *) 18 + let query = Printf.sprintf "?name=%s%s" 19 + (Uri.pct_encode name) 20 + (if with_hidden then "&withHidden=true" else "") in 21 + let url = base_url ^ "/people/search" ^ query in 22 + let response = Requests.get session url in 23 + if Requests.Response.ok response then begin 24 + let json = Requests.Response.json response in 25 + let people = Openapi.Runtime.Json.decode_json_exn 26 + (Jsont.list Immich.Person.ResponseDto.jsont) json in 27 + if people = [] then 28 + Fmt.pr "No people found matching '%s'.@." name 29 + else begin 30 + Fmt.pr "People matching '%s':@." name; 31 + List.iter (fun person -> 32 + let name = Immich.Person.ResponseDto.name person in 33 + let id = Immich.Person.ResponseDto.id person in 34 + let hidden = if Immich.Person.ResponseDto.is_hidden person then " (hidden)" else "" in 35 + let display_name = if name = "" then "<unnamed>" else name in 36 + Fmt.pr " %s - %s%s@." id display_name hidden 37 + ) people 38 + end 39 + end else begin 40 + (* Raise API error for proper handling *) 41 + raise (Openapi.Runtime.Api_error { 42 + operation = "search_people"; 43 + method_ = "GET"; 44 + url; 45 + status = Requests.Response.status_code response; 46 + body = Requests.Response.text response; 47 + }) 37 48 end 38 - end else begin 39 - Fmt.epr "Search failed: %d@." (Requests.Response.status_code response); 40 - exit 1 41 - end 42 - ) env 49 + ) env 50 + ) 43 51 44 52 let name_arg = 45 53 let doc = "Name to search for." in ··· 69 77 Arg.(required & pos 0 (some string) None & info [] ~docv:"PERSON_ID" ~doc) 70 78 71 79 let thumbnail_action ~requests_config ~profile ~person_id ~output env = 72 - Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client -> 73 - let api = Immich_auth.Client.client client in 74 - let session = Immich.session api in 75 - let base_url = Immich.base_url api in 76 - let url = Printf.sprintf "%s/people/%s/thumbnail" base_url person_id in 77 - let response = Requests.get session url in 78 - if Requests.Response.ok response then begin 79 - let data = Requests.Response.text response in 80 - if output = "-" then 81 - print_string data 82 - else begin 83 - let oc = open_out_bin output in 84 - output_string oc data; 85 - close_out oc; 86 - Fmt.pr "Thumbnail saved to %s@." output 80 + Immich_auth.Error.wrap (fun () -> 81 + Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client -> 82 + let api = Immich_auth.Client.client client in 83 + let session = Immich.session api in 84 + let base_url = Immich.base_url api in 85 + let url = Printf.sprintf "%s/people/%s/thumbnail" base_url person_id in 86 + let response = Requests.get session url in 87 + if Requests.Response.ok response then begin 88 + let data = Requests.Response.text response in 89 + if output = "-" then 90 + print_string data 91 + else begin 92 + let oc = open_out_bin output in 93 + output_string oc data; 94 + close_out oc; 95 + Fmt.pr "Thumbnail saved to %s@." output 96 + end 97 + end else begin 98 + raise (Openapi.Runtime.Api_error { 99 + operation = "get_person_thumbnail"; 100 + method_ = "GET"; 101 + url; 102 + status = Requests.Response.status_code response; 103 + body = Requests.Response.text response; 104 + }) 87 105 end 88 - end else begin 89 - Fmt.epr "Failed to get thumbnail: %d@." (Requests.Response.status_code response); 90 - exit 1 91 - end 92 - ) env 106 + ) env 107 + ) 93 108 94 109 let thumbnail_cmd env fs = 95 110 let doc = "Download a person's thumbnail image." in ··· 103 118 (* List all people *) 104 119 105 120 let list_action ~requests_config ~profile ~with_hidden env = 106 - Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client -> 107 - let api = Immich_auth.Client.client client in 108 - let with_hidden_param = if with_hidden then Some "true" else None in 109 - let resp = Immich.People.get_all_people api ?with_hidden:with_hidden_param () in 110 - let people = Immich.People.ResponseDto.people resp in 111 - let total = Immich.People.ResponseDto.total resp in 112 - let hidden = Immich.People.ResponseDto.hidden resp in 113 - Fmt.pr "People: %d total, %d hidden@." total hidden; 114 - if people = [] then 115 - Fmt.pr "No people found.@." 116 - else begin 117 - List.iter (fun person -> 118 - let name = Immich.Person.ResponseDto.name person in 119 - let id = Immich.Person.ResponseDto.id person in 120 - let is_hidden = Immich.Person.ResponseDto.is_hidden person in 121 - let hidden_marker = if is_hidden then " (hidden)" else "" in 122 - let display_name = if name = "" then "<unnamed>" else name in 123 - Fmt.pr " %s - %s%s@." id display_name hidden_marker 124 - ) people 125 - end 126 - ) env 121 + Immich_auth.Error.wrap (fun () -> 122 + Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client -> 123 + let api = Immich_auth.Client.client client in 124 + let with_hidden_param = if with_hidden then Some "true" else None in 125 + let resp = Immich.People.get_all_people api ?with_hidden:with_hidden_param () in 126 + let people = Immich.People.ResponseDto.people resp in 127 + let total = Immich.People.ResponseDto.total resp in 128 + let hidden = Immich.People.ResponseDto.hidden resp in 129 + Fmt.pr "People: %d total, %d hidden@." total hidden; 130 + if people = [] then 131 + Fmt.pr "No people found.@." 132 + else begin 133 + List.iter (fun person -> 134 + let name = Immich.Person.ResponseDto.name person in 135 + let id = Immich.Person.ResponseDto.id person in 136 + let is_hidden = Immich.Person.ResponseDto.is_hidden person in 137 + let hidden_marker = if is_hidden then " (hidden)" else "" in 138 + let display_name = if name = "" then "<unnamed>" else name in 139 + Fmt.pr " %s - %s%s@." id display_name hidden_marker 140 + ) people 141 + end 142 + ) env 143 + ) 127 144 128 145 let list_cmd env fs = 129 146 let doc = "List all people." in
+10 -3
ocaml-immich/bin/main.ml
··· 30 30 ] 31 31 in 32 32 Cmd.eval (Cmd.group info cmds) 33 - with exn -> 34 - Fmt.epr "Eio_main.run raised: %s@." (Printexc.to_string exn); 35 - 125 33 + with 34 + | Openapi.Runtime.Api_error _ as exn -> 35 + (* Handle Immich API errors with nice formatting *) 36 + Immich_auth.Error.handle_exn exn 37 + | Failure msg -> 38 + Fmt.epr "Error: %s@." msg; 39 + 1 40 + | exn -> 41 + Fmt.epr "Unexpected error: %s@." (Printexc.to_string exn); 42 + 125 36 43 in 37 44 exit exit_code
+1
ocaml-immich/lib/dune
··· 2 2 (name immich_auth) 3 3 (libraries 4 4 immich 5 + openapi 5 6 requests 6 7 jsont 7 8 jsont.bytesrw
+106
ocaml-immich/lib/error.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Immich API error handling using NestJS error format. *) 7 + 8 + (** Re-export NestJS error type for convenience. *) 9 + type t = Openapi.Nestjs.t = { 10 + status_code : int; 11 + error : string option; 12 + message : string; 13 + correlation_id : string option; 14 + } 15 + 16 + (** Parse an API error into a structured Immich/NestJS error. *) 17 + let of_api_error = Openapi.Nestjs.of_api_error 18 + 19 + (** Pretty-print an Immich API error. 20 + 21 + Format: "Permission denied: person.read [403] (correlationId: abc123)" *) 22 + let pp ppf (e : t) = 23 + let error_prefix = match e.error with 24 + | Some err -> err ^ ": " 25 + | None -> "" 26 + in 27 + match e.correlation_id with 28 + | Some cid -> 29 + Format.fprintf ppf "%s%s [%d] (correlationId: %s)" 30 + error_prefix e.message e.status_code cid 31 + | None -> 32 + Format.fprintf ppf "%s%s [%d]" error_prefix e.message e.status_code 33 + 34 + (** Convert to a human-readable string. *) 35 + let to_string (e : t) : string = 36 + Format.asprintf "%a" pp e 37 + 38 + (** Check if this is an authentication/authorization error. *) 39 + let is_auth_error = Openapi.Nestjs.is_auth_error 40 + 41 + (** Check if this is a "not found" error. *) 42 + let is_not_found = Openapi.Nestjs.is_not_found 43 + 44 + (** Handle an exception, printing a nice error message if it's an API error. 45 + 46 + Returns an exit code: 47 + - 0 if not an error (shouldn't happen, but for completeness) 48 + - 1 for most API errors 49 + - 77 for authentication errors (permission denied) 50 + - 69 for not found errors *) 51 + let handle_exn exn = 52 + match exn with 53 + | Openapi.Runtime.Api_error e -> 54 + (match of_api_error e with 55 + | Some nestjs -> 56 + Fmt.epr "@[<v>Error: %a@]@." pp nestjs; 57 + if is_auth_error nestjs then 77 58 + else if is_not_found nestjs then 69 59 + else 1 60 + | None -> 61 + (* Not a NestJS error, show raw response *) 62 + Fmt.epr "@[<v>API Error: %s %s returned %d@,%s@]@." 63 + e.method_ e.url e.status e.body; 64 + 1) 65 + | Failure msg -> 66 + Fmt.epr "Error: %s@." msg; 67 + 1 68 + | exn -> 69 + (* Re-raise unknown exceptions *) 70 + raise exn 71 + 72 + (** Wrap a function to handle API errors gracefully. 73 + 74 + Usage: 75 + {[ 76 + let () = Immich_auth.Error.run (fun () -> 77 + let albums = Immich.Albums.get_all_albums client () in 78 + ... 79 + ) 80 + ]} *) 81 + let run f = 82 + try f (); 0 83 + with exn -> handle_exn exn 84 + 85 + (** Wrap a command action to handle API errors gracefully. 86 + 87 + This is designed to be used in cmdliner command definitions: 88 + {[ 89 + let list_action ~profile env = 90 + Immich_auth.Error.wrap (fun () -> 91 + let api = ... in 92 + let albums = Immich.Albums.get_all_albums api () in 93 + ... 94 + ) 95 + 96 + let list_cmd env fs = 97 + Cmd.v info Term.(const list_action $ ...) 98 + ]} 99 + 100 + The wrapper catches API errors and prints a nice message, 101 + then exits with an appropriate code. *) 102 + let wrap f = 103 + try f () 104 + with exn -> 105 + let code = handle_exn exn in 106 + exit code
+96
ocaml-immich/lib/error.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Immich API error handling using NestJS error format. 7 + 8 + The Immich API (built with NestJS) returns 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 utilities for parsing and displaying these errors. *) 19 + 20 + (** {1 Error Type} *) 21 + 22 + type t = Openapi.Nestjs.t = { 23 + status_code : int; 24 + error : string option; 25 + message : string; 26 + correlation_id : string option; 27 + } 28 + (** A structured Immich API error. *) 29 + 30 + (** {1 Parsing} *) 31 + 32 + val of_api_error : Openapi.Runtime.api_error -> t option 33 + (** Parse an API error into a structured Immich/NestJS error. 34 + Returns [None] if the error body is not valid NestJS JSON. *) 35 + 36 + (** {1 Pretty Printing} *) 37 + 38 + val pp : Format.formatter -> t -> unit 39 + (** Pretty-print an Immich API error. 40 + 41 + Format: "Forbidden: Missing required permission [403] (correlationId: abc123)" *) 42 + 43 + val to_string : t -> string 44 + (** Convert to a human-readable string. *) 45 + 46 + (** {1 Error Classification} *) 47 + 48 + val is_auth_error : t -> bool 49 + (** Check if this is an authentication/authorization error (401 or 403). *) 50 + 51 + val is_not_found : t -> bool 52 + (** Check if this is a "not found" error (404). *) 53 + 54 + (** {1 Exception Handling} *) 55 + 56 + val handle_exn : exn -> int 57 + (** Handle an exception, printing a nice error message if it's an API error. 58 + 59 + Returns an exit code: 60 + - 1 for most API errors 61 + - 77 for authentication errors (permission denied) 62 + - 69 for not found errors 63 + 64 + @raise exn if the exception is not an API error or Failure *) 65 + 66 + val run : (unit -> unit) -> int 67 + (** Wrap a function to handle API errors gracefully. 68 + 69 + Returns 0 on success, or an appropriate exit code on error. 70 + 71 + Usage: 72 + {[ 73 + let () = 74 + let code = Immich_auth.Error.run (fun () -> 75 + let albums = Immich.Albums.get_all_albums client () in 76 + ... 77 + ) in 78 + exit code 79 + ]} *) 80 + 81 + val wrap : (unit -> unit) -> unit 82 + (** Wrap a command action to handle API errors gracefully. 83 + 84 + This is designed to be used in cmdliner command definitions. 85 + Catches API errors, prints a nice message, and exits with 86 + an appropriate code. 87 + 88 + Usage: 89 + {[ 90 + let list_action ~profile env = 91 + Immich_auth.Error.wrap (fun () -> 92 + let api = ... in 93 + let albums = Immich.Albums.get_all_albums api () in 94 + ... 95 + ) 96 + ]} *)
+1
ocaml-immich/lib/immich_auth.ml
··· 11 11 module Session = Session 12 12 module Client = Client 13 13 module Cmd = Cmd 14 + module Error = Error
+2
ocaml-openapi/lib/openapi.ml
··· 4 4 - {!module:Spec} - OpenAPI 3.x specification types with jsont codecs 5 5 - {!module:Codegen} - Code generation from spec to OCaml 6 6 - {!module:Runtime} - Runtime utilities for generated clients 7 + - {!module:Nestjs} - NestJS/Express error handling (optional) 7 8 *) 8 9 9 10 module Spec = Openapi_spec 10 11 module Codegen = Openapi_codegen 11 12 module Runtime = Openapi_runtime 13 + module Nestjs = Openapi_nestjs
+156
ocaml-openapi/lib/openapi_nestjs.ml
··· 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. *) 41 + type 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"] ]} *) 63 + let 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. *) 93 + let 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. *) 100 + let 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). *) 106 + let 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). *) 110 + let is_not_found (e : t) : bool = 111 + e.status_code = 404 112 + 113 + (** Check if this is a validation error (400 with message array). *) 114 + let is_validation_error (e : t) : bool = 115 + e.status_code = 400 116 + 117 + (** Check if this is a server error (5xx). *) 118 + let 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. *) 124 + let 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. *) 133 + let 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. *) 140 + exception Error of t 141 + 142 + (** Register a pretty printer for the exception. *) 143 + let () = 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) *) 153 + let 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)
+104
ocaml-openapi/lib/openapi_nestjs.mli
··· 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. *) 41 + type 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 + val jsont : t Jsont.t 59 + (** Jsont codec for NestJS errors. *) 60 + 61 + (** {1 Parsing} *) 62 + 63 + val of_string : string -> t option 64 + (** Parse a JSON string into a NestJS error. 65 + Returns [None] if the string is not valid NestJS error JSON. *) 66 + 67 + val of_api_error : Openapi_runtime.api_error -> t option 68 + (** Parse an {!Openapi_runtime.api_error} into a structured NestJS error. 69 + Returns [None] if the error body is not valid NestJS error JSON. *) 70 + 71 + (** {1 Convenience Functions} *) 72 + 73 + val is_auth_error : t -> bool 74 + (** Check if this is a permission/authorization error (401 or 403). *) 75 + 76 + val is_not_found : t -> bool 77 + (** Check if this is a "not found" error (404). *) 78 + 79 + val is_validation_error : t -> bool 80 + (** Check if this is a validation error (400 with message array). *) 81 + 82 + val is_server_error : t -> bool 83 + (** Check if this is a server error (5xx). *) 84 + 85 + (** {1 Pretty Printing} *) 86 + 87 + val pp : Format.formatter -> t -> unit 88 + (** Pretty-print a NestJS error. *) 89 + 90 + val to_string : t -> string 91 + (** Convert to a human-readable string. *) 92 + 93 + (** {1 Exception Handling} *) 94 + 95 + exception Error of t 96 + (** Exception for NestJS-specific errors. 97 + Use this when you want to distinguish NestJS errors from generic API errors. *) 98 + 99 + val raise_if_nestjs : Openapi_runtime.api_error -> 'a 100 + (** Handle an {!Openapi_runtime.api_error}, converting it to a NestJS error 101 + if possible. 102 + 103 + @raise Error if the error body parses as a NestJS error 104 + @raise Openapi_runtime.Api_error if parsing fails (re-raises original) *)