forked from
anil.recoil.org/ocaml-requests
A batteries included HTTP/1.1 client in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** HTTP response handling per
7 {{:https://datatracker.ietf.org/doc/html/rfc9110#section-15}RFC 9110}
8
9 This module represents HTTP responses and provides functions to access
10 status codes, headers, and response bodies. Responses support streaming to
11 efficiently handle large payloads.
12
13 Caching semantics follow
14 {{:https://datatracker.ietf.org/doc/html/rfc9111}RFC 9111} (HTTP Caching).
15
16 {2 Examples}
17
18 {[
19 (* Check response status *)
20 if Response.ok response then
21 Printf.printf "Success!\n"
22 else
23 Printf.printf "Error: %d\n" (Response.status_code response);
24
25 (* Access headers *)
26 match Response.content_type response with
27 | Some mime -> Printf.printf "Type: %s\n" (Mime.to_string mime)
28 | None -> ()
29
30 (* Stream response body *)
31 let body = Response.body response in
32 Eio.Flow.copy body (Eio.Flow.buffer_sink buffer)
33
34 (* Response automatically closes when the switch is released *)
35 ]}
36
37 {b Note}: Responses are automatically closed when the switch they were
38 created with is released. Manual cleanup is not necessary. *)
39
40open Eio
41
42val src : Logs.Src.t
43(** Log source for response operations. *)
44
45type t
46(** Abstract response type representing an HTTP response. *)
47
48val v :
49 sw:Eio.Switch.t ->
50 status:int ->
51 headers:Headers.t ->
52 body:Eio.Flow.source_ty Eio.Resource.t ->
53 url:string ->
54 elapsed:float ->
55 t
56(** [v ~sw ~status ~headers ~body ~url ~elapsed] creates a response. Internal
57 function primarily used for caching. *)
58
59(** {1 Status Information} *)
60
61val status : t -> Status.t
62(** [status response] returns the HTTP status as a {!Status.t} value. *)
63
64val status_code : t -> int
65(** [status_code response] returns the HTTP status code as an integer (e.g.,
66 200, 404). *)
67
68val ok : t -> bool
69(** [ok response] returns [true] if the status code is in the 2xx success range.
70 This is an alias for {!Status.is_success}. *)
71
72(** {1 Header Access} *)
73
74val headers : t -> Headers.t
75(** [headers response] returns all response headers. *)
76
77val header : Header_name.t -> t -> string option
78(** [header name response] returns the value of a specific header, or [None] if
79 not present. Header names are case-insensitive.
80
81 Example: [header `Content_type response]. *)
82
83val header_string : string -> t -> string option
84(** [header_string name response] returns the value of a header by string name.
85 Use this when header names come from external sources (e.g., wire format).
86 Header names are case-insensitive. *)
87
88val content_type : t -> Mime.t option
89(** [content_type response] returns the parsed Content-Type header as a MIME
90 type, or [None] if the header is not present or cannot be parsed. *)
91
92val content_length : t -> int64 option
93(** [content_length response] returns the Content-Length in bytes, or [None] if
94 not specified or chunked encoding is used. *)
95
96val location : t -> string option
97(** [location response] returns the Location header value, typically used in
98 redirects. Returns [None] if the header is not present. *)
99
100(** {1 Conditional Request / Caching Headers}
101
102 Per Recommendation #19: Conditional Request Helpers (ETag/Last-Modified) RFC
103 9110 Section 8.8.2-8.8.3 *)
104
105val etag : t -> string option
106(** [etag response] returns the ETag header value, which is an opaque identifier
107 for a specific version of a resource. Use with {!Headers.if_none_match} for
108 conditional requests. Example: ["\"abc123\""] or [W/"abc123"] (weak
109 validator). *)
110
111val last_modified : t -> string option
112(** [last_modified response] returns the Last-Modified header as a raw string.
113 Format: HTTP-date (e.g., ["Sun, 06 Nov 1994 08:49:37 GMT"]). *)
114
115val parse_http_date : string -> Ptime.t option
116(** [parse_http_date s] parses an HTTP-date string (RFC 9110 Section 5.6.7) to
117 Ptime.t. Supports RFC 1123, RFC 850, and ANSI C asctime() formats. Returns
118 [None] if parsing fails.
119
120 This is exposed for use by other modules that need to parse HTTP dates. *)
121
122val last_modified_ptime : t -> Ptime.t option
123(** [last_modified_ptime response] parses the Last-Modified header as a Ptime.t.
124 Returns [None] if the header is not present or cannot be parsed. *)
125
126val date : t -> string option
127(** [date response] returns the Date header (time response was generated). *)
128
129val date_ptime : t -> Ptime.t option
130(** [date_ptime response] parses the Date header as a Ptime.t. *)
131
132val expires : t -> string option
133(** [expires response] returns the Expires header (HTTP/1.0 cache control).
134 Prefer using {!cache_control} for RFC 9111 compliant caching. *)
135
136val expires_ptime : t -> Ptime.t option
137(** [expires_ptime response] parses the Expires header as a Ptime.t. *)
138
139val age : t -> int option
140(** [age response] returns the Age header value in seconds. The Age header
141 indicates how long the response has been in a cache. *)
142
143(** {1 Cache-Control Parsing}
144
145 Per Recommendation #17: Response Caching with RFC 7234/9111 Compliance *)
146
147val cache_control : t -> Cache_control.response option
148(** [cache_control response] parses and returns the Cache-Control header
149 directives. Returns [None] if the header is not present.
150
151 Example:
152 {[
153 match Response.cache_control response with
154 | Some cc when cc.Cache_control.no_store -> "Do not cache"
155 | Some cc -> Fmt.str "Max age: %d" (Option.get cc.max_age)
156 | None -> "No cache directives"
157 ]} *)
158
159val cache_control_raw : t -> string option
160(** [cache_control_raw response] returns the raw Cache-Control header string
161 without parsing. Useful for debugging or custom parsing. *)
162
163val is_cacheable : t -> bool
164(** [is_cacheable response] returns [true] if the response may be cached based
165 on its status code and Cache-Control directives. A response is cacheable if
166 no-store is not present and either:
167 - Status is cacheable by default (200, 203, 204, 206, 300, 301, 308, 404,
168 405, 410, 414, 501)
169 - Explicit caching directive (max-age, s-maxage) is present. *)
170
171val freshness_lifetime : t -> int option
172(** [freshness_lifetime response] calculates how long the response is fresh in
173 seconds, based on Cache-Control max-age or Expires header. Returns [None] if
174 freshness cannot be determined. *)
175
176val must_revalidate : t -> bool
177(** [must_revalidate response] returns [true] if cached copies must be
178 revalidated with the origin server before use (must-revalidate,
179 proxy-revalidate, or no-cache directive present). *)
180
181val is_stale : now:Ptime.t -> t -> bool
182(** [is_stale ~now response] returns [true] if the response's freshness lifetime
183 has expired. Requires the current time as [now]. Returns [false] if
184 staleness cannot be determined. *)
185
186val is_not_modified : t -> bool
187(** [is_not_modified response] returns [true] if this is a 304 Not Modified
188 response, indicating the cached version is still valid. *)
189
190val vary : t -> string option
191(** [vary response] returns the Vary header, which lists request headers that
192 affect the response (for cache key construction). *)
193
194val vary_headers : t -> string list
195(** [vary_headers response] parses the Vary header into a list of header names.
196 Returns an empty list if Vary is not present. *)
197
198(** {1 Response Metadata} *)
199
200val url : t -> string
201(** [url response] returns the final URL after following any redirects. This may
202 differ from the originally requested URL. *)
203
204val elapsed : t -> float
205(** [elapsed response] returns the time taken for the request in seconds,
206 including connection establishment, sending the request, and receiving
207 headers. *)
208
209(** {1 Response Body} *)
210
211val body : t -> Flow.source_ty Resource.t
212(** [body response] returns the response body as an Eio flow for streaming. This
213 allows efficient processing of large responses without loading them entirely
214 into memory.
215
216 Example:
217 {[
218 let body = Response.body response in
219 let buffer = Buffer.create 4096 in
220 Eio.Flow.copy body (Eio.Flow.buffer_sink buffer);
221 Buffer.contents buffer
222 ]} *)
223
224val text : t -> string
225(** [text response] reads and returns the entire response body as a string. The
226 response body is fully consumed by this operation.
227
228 @raise Failure if the response has already been closed. *)
229
230val json : t -> Jsont.json
231(** [json response] parses the response body as JSON. The response body is fully
232 consumed by this operation.
233
234 Example:
235 {[
236 let json = Response.json response in
237 process_json json
238 ]}
239
240 @raise Eio.Io with {!Error.Json_parse_error} if JSON parsing fails.
241 @raise Failure if the response has already been closed. *)
242
243val jsonv : 'a Jsont.t -> t -> 'a
244(** [jsonv codec response] parses the response body as JSON and decodes it to a
245 typed value using the provided [codec]. The response body is fully consumed
246 by this operation.
247
248 This is the preferred way to decode JSON responses into typed OCaml values,
249 as it provides type safety and works with custom record types.
250
251 Example:
252 {[
253 (* Define a codec for your type *)
254 type user = { name : string; age : int }
255
256 let user_codec =
257 Jsont.Obj.map ~kind:"user" (fun name age -> { name; age })
258 |> Jsont.Obj.mem "name" Jsont.string ~enc:(fun u -> u.name)
259 |> Jsont.Obj.mem "age" Jsont.int ~enc:(fun u -> u.age)
260 |> Jsont.Obj.finish
261
262 (* Decode the response to a typed value *)
263 let user = Response.jsonv user_codec response in
264 Printf.printf "User: %s, age %d\n" user.name user.age
265 ]}
266
267 @raise Eio.Io with {!Error.Json_parse_error} if JSON parsing fails.
268 @raise Failure if the response has already been closed. *)
269
270val raise_for_status : t -> t
271(** [raise_for_status response] raises [Eio.Io] with [Error.Http_error] if the
272 response status code indicates an error (>= 400). Returns the response
273 unchanged if the status indicates success (< 400).
274
275 This is useful for failing fast on HTTP errors:
276 {[
277 let response = Requests.get req url |> Response.raise_for_status in
278 (* Only reaches here if status < 400 *)
279 process_success response
280 ]}
281
282 @raise Eio.Io with [Error.Http_error] if status code >= 400. *)
283
284val check_status : t -> (t, Error.t) result
285(** [check_status response] returns [Ok response] if the status code is < 400,
286 or [Error error] if the status code indicates an error (>= 400).
287
288 This provides functional error handling without exceptions, complementing
289 {!raise_for_status} for different coding styles.
290
291 Example:
292 {[
293 match Response.check_status response with
294 | Ok resp -> process_success resp
295 | Error err -> handle_error err
296 ]}
297
298 Per Recommendation #21: Provides a Result-based alternative to
299 raise_for_status. *)
300
301(** {1 Pretty Printing} *)
302
303val pp : Format.formatter -> t -> unit
304(** Pretty print a response summary. *)
305
306val pp_detailed : Format.formatter -> t -> unit
307(** Pretty print a response with full headers. *)
308
309(** {1 Private API} *)
310
311(** Internal functions exposed for use by other modules in the library. These
312 are not part of the public API and may change between versions. *)
313module Private : sig
314 val make :
315 sw:Eio.Switch.t ->
316 status:int ->
317 headers:Headers.t ->
318 body:Flow.source_ty Resource.t ->
319 url:string ->
320 elapsed:float ->
321 t
322 (** [make ~sw ~status ~headers ~body ~url ~elapsed] constructs a response. The
323 response will be automatically closed when the switch is released. This
324 function is used internally by the Client module. *)
325end