A batteries included HTTP/1.1 client in OCaml
at claude-test 166 lines 5.7 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** Centralized error handling for the Requests library using Eio.Io exceptions. 7 8 This module follows the Eio.Io exception pattern for structured error handling, 9 providing granular error types and query functions for smart retry logic. 10 11 {2 Usage} 12 13 Errors are raised using the Eio.Io pattern: 14 {[ 15 raise (Error.err (Error.Timeout { operation = "connect"; duration = Some 30.0 })) 16 ]} 17 18 To catch and handle errors: 19 {[ 20 try 21 (* ... HTTP request ... *) 22 with 23 | Eio.Io (Error.E e, _) when Error.is_retryable e -> 24 (* Retry the request *) 25 | Eio.Io (Error.E e, _) -> 26 Printf.eprintf "Request failed: %s\n" (Error.to_string e) 27 ]} 28*) 29 30(** Log source for error reporting *) 31val src : Logs.Src.t 32 33(** {1 Error Type} 34 35 Granular error variants with contextual information. 36 Each variant contains a record with relevant details. *) 37 38type error = 39 (* Timeout errors *) 40 | Timeout of { operation: string; duration: float option } 41 42 (* Redirect errors *) 43 | Too_many_redirects of { url: string; count: int; max: int } 44 | Invalid_redirect of { url: string; reason: string } 45 46 (* HTTP response errors *) 47 (* Note: headers stored as list to avoid dependency cycle with Headers module *) 48 | Http_error of { 49 url: string; 50 status: int; 51 reason: string; 52 body_preview: string option; 53 headers: (string * string) list 54 } 55 56 (* Authentication errors *) 57 | Authentication_failed of { url: string; reason: string } 58 59 (* Connection errors - granular breakdown *) 60 | Dns_resolution_failed of { hostname: string } 61 | Tcp_connect_failed of { host: string; port: int; reason: string } 62 | Tls_handshake_failed of { host: string; reason: string } 63 64 (* Security-related errors *) 65 | Invalid_header of { name: string; reason: string } 66 | Body_too_large of { limit: int64; actual: int64 option } 67 | Headers_too_large of { limit: int; actual: int } 68 | Decompression_bomb of { limit: int64; ratio: float } 69 | Content_length_mismatch of { expected: int64; actual: int64 } 70 | Insecure_auth of { url: string; auth_type: string } 71 (** Per RFC 7617 Section 4 and RFC 6750 Section 5.1: 72 Basic, Bearer, and Digest authentication over unencrypted HTTP 73 exposes credentials to eavesdropping. Raised when attempting 74 to use these auth methods over HTTP without explicit opt-out. *) 75 76 (* JSON errors *) 77 | Json_parse_error of { body_preview: string; reason: string } 78 | Json_encode_error of { reason: string } 79 80 (* Other errors *) 81 | Proxy_error of { host: string; reason: string } 82 | Encoding_error of { encoding: string; reason: string } 83 | Invalid_url of { url: string; reason: string } 84 | Invalid_request of { reason: string } 85 86(** {1 Eio.Exn Integration} *) 87 88(** Extension of [Eio.Exn.err] for Requests errors *) 89type Eio.Exn.err += E of error 90 91(** Create an Eio exception from an error. 92 Usage: [raise (err (Timeout { operation = "read"; duration = Some 5.0 }))] *) 93val err : error -> exn 94 95(** {1 URL and Credential Sanitization} *) 96 97(** Remove userinfo (username:password) from a URL for safe logging *) 98val sanitize_url : string -> string 99 100(** Redact sensitive headers (Authorization, Cookie, etc.) for safe logging. 101 Takes and returns a list of (name, value) pairs. *) 102val sanitize_headers : (string * string) list -> (string * string) list 103 104(** Check if a header name is sensitive (case-insensitive) *) 105val is_sensitive_header : string -> bool 106 107(** {1 Pretty Printing} *) 108 109(** Pretty printer for error values *) 110val pp_error : Format.formatter -> error -> unit 111 112(** {1 Query Functions} 113 114 These functions enable smart error handling without pattern matching. *) 115 116(** Returns [true] if the error is a timeout *) 117val is_timeout : error -> bool 118 119(** Returns [true] if the error is a DNS resolution failure *) 120val is_dns : error -> bool 121 122(** Returns [true] if the error is a TLS handshake failure *) 123val is_tls : error -> bool 124 125(** Returns [true] if the error is any connection-related failure 126 (DNS, TCP connect, or TLS handshake) *) 127val is_connection : error -> bool 128 129(** Returns [true] if the error is an HTTP response error *) 130val is_http_error : error -> bool 131 132(** Returns [true] if the error is a client error (4xx status or similar) *) 133val is_client_error : error -> bool 134 135(** Returns [true] if the error is a server error (5xx status) *) 136val is_server_error : error -> bool 137 138(** Returns [true] if the error is typically retryable. 139 Retryable errors include: timeouts, connection errors, 140 and certain HTTP status codes (408, 429, 500, 502, 503, 504) *) 141val is_retryable : error -> bool 142 143(** Returns [true] if the error is security-related 144 (header injection, body too large, decompression bomb, etc.) *) 145val is_security_error : error -> bool 146 147(** Returns [true] if the error is a JSON parsing or encoding error *) 148val is_json_error : error -> bool 149 150(** {1 Error Extraction} *) 151 152(** Extract error from an Eio.Io exception, if it's a Requests error *) 153val of_eio_exn : exn -> error option 154 155(** {1 HTTP Status Helpers} *) 156 157(** Get the HTTP status code from an error, if applicable *) 158val get_http_status : error -> int option 159 160(** Get the URL associated with an error, if applicable *) 161val get_url : error -> string option 162 163(** {1 String Conversion} *) 164 165(** Convert error to human-readable string *) 166val to_string : error -> string