OCaml bindings to the Typesense embeddings search API
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Typesense API error handling.
7
8 Typesense returns errors in a simple format: {"message": "..."} *)
9
10(** Typesense error response *)
11type t = {
12 message : string;
13 status_code : int;
14}
15
16let error_jsont =
17 Jsont.Object.map ~kind:"TypesenseError"
18 (fun message -> message)
19 |> Jsont.Object.mem "message" Jsont.string ~enc:Fun.id
20 |> Jsont.Object.skip_unknown
21 |> Jsont.Object.finish
22
23(** Parse an API error into a structured Typesense error. *)
24let of_api_error (e : Openapi.Runtime.api_error) : t option =
25 match Jsont_bytesrw.decode_string error_jsont e.body with
26 | Ok message -> Some { message; status_code = e.status }
27 | Error _ -> None
28
29(** {1 Styled Output Helpers} *)
30
31(** Style for error labels (red, bold) *)
32let error_style = Fmt.(styled (`Fg `Red) (styled `Bold string))
33
34(** Style for status codes *)
35let status_style status =
36 if status >= 500 then Fmt.(styled (`Fg `Red) int)
37 else if status >= 400 then Fmt.(styled (`Fg `Yellow) int)
38 else Fmt.(styled (`Fg `Green) int)
39
40(** Pretty-print a Typesense API error with colors. *)
41let pp ppf (e : t) =
42 Fmt.pf ppf "%s [%a]" e.message (status_style e.status_code) e.status_code
43
44(** Convert to a human-readable string (without colors). *)
45let to_string (e : t) : string =
46 Printf.sprintf "%s [%d]" e.message e.status_code
47
48(** Check if this is an authentication/authorization error. *)
49let is_auth_error (e : t) =
50 e.status_code = 401 || e.status_code = 403
51
52(** Check if this is a "not found" error. *)
53let is_not_found (e : t) =
54 e.status_code = 404
55
56(** Handle an exception, printing a nice error message if it's an API error.
57
58 Returns an exit code:
59 - 1 for most API errors
60 - 77 for authentication errors (permission denied)
61 - 69 for not found errors *)
62let handle_exn exn =
63 match exn with
64 | Openapi.Runtime.Api_error e ->
65 (match of_api_error e with
66 | Some err ->
67 Fmt.epr "%a %a@." error_style "Error:" pp err;
68 if is_auth_error err then 77
69 else if is_not_found err then 69
70 else 1
71 | None ->
72 (* Not a Typesense error, show raw response *)
73 Fmt.epr "%a %s %s returned %a@.%s@."
74 error_style "API Error:"
75 e.method_ e.url
76 (status_style e.status) e.status
77 e.body;
78 1)
79 | Failure msg ->
80 Fmt.epr "%a %s@." error_style "Error:" msg;
81 1
82 | exn ->
83 (* Re-raise unknown exceptions *)
84 raise exn
85
86(** Wrap a function to handle API errors gracefully. *)
87let run f =
88 try f (); 0
89 with exn -> handle_exn exn
90
91(** Exception to signal desired exit code without calling [exit] directly.
92 This avoids issues when running inside Eio's event loop. *)
93exception Exit_code of int
94
95(** Wrap a command action to handle API errors gracefully. *)
96let wrap f =
97 try f ()
98 with
99 | Stdlib.Exit ->
100 (* exit() was called somewhere - treat as success *)
101 ()
102 | Eio.Cancel.Cancelled Stdlib.Exit ->
103 (* Eio wraps Exit in Cancelled - treat as success *)
104 ()
105 | exn ->
106 let code = handle_exn exn in
107 raise (Exit_code code)