ocaml http/1, http/2 and websocket client and server library
1(** Unified HTTP Client supporting HTTP/1.1 and HTTP/2.
2
3 This module provides a high-level HTTP client that automatically selects the
4 appropriate protocol based on ALPN negotiation or configuration. Connections
5 are pooled for high performance. *)
6
7type protocol = HTTP_1_1 | HTTP_2
8
9type config = {
10 connect_timeout : float;
11 read_timeout : float;
12 write_timeout : float;
13 follow_redirects : int option;
14 preferred_protocol : protocol option;
15 buffer_size : int;
16 max_response_body : int64 option;
17 tls : Tls_config.Client.t;
18 default_headers : (string * string) list;
19 pool : Pool.config;
20}
21
22let default_config =
23 {
24 connect_timeout = 30.0;
25 read_timeout = 30.0;
26 write_timeout = 30.0;
27 follow_redirects = Some 10;
28 preferred_protocol = Some HTTP_1_1;
29 buffer_size = 16384;
30 max_response_body = None;
31 tls = Tls_config.Client.default;
32 default_headers = [ ("User-Agent", "hcs/" ^ Build_info.version) ];
33 pool = Pool.default_config;
34 }
35
36let with_timeout timeout config =
37 { config with connect_timeout = timeout; read_timeout = timeout }
38
39let with_connect_timeout timeout config =
40 { config with connect_timeout = timeout }
41
42let with_read_timeout timeout config = { config with read_timeout = timeout }
43let with_write_timeout timeout config = { config with write_timeout = timeout }
44
45let with_redirects max_redirects config =
46 { config with follow_redirects = Some max_redirects }
47
48let without_redirects config = { config with follow_redirects = None }
49let with_buffer_size size config = { config with buffer_size = size }
50
51let with_max_response_body max_size config =
52 { config with max_response_body = Some max_size }
53
54let with_tls tls config = { config with tls }
55let with_insecure_tls config = { config with tls = Tls_config.Client.insecure }
56
57let with_http2 config =
58 { config with preferred_protocol = Some HTTP_2; tls = Tls_config.Client.h2 }
59
60let with_http11 config =
61 {
62 config with
63 preferred_protocol = Some HTTP_1_1;
64 tls = Tls_config.Client.default;
65 }
66
67let with_default_header name value config =
68 { config with default_headers = (name, value) :: config.default_headers }
69
70let with_default_headers headers config =
71 { config with default_headers = headers @ config.default_headers }
72
73let with_pool_config pool config = { config with pool }
74
75let with_max_connections max_conn config =
76 {
77 config with
78 pool = { config.pool with max_connections_per_host = max_conn };
79 }
80
81type error =
82 | Connection_failed of string
83 | Tls_error of string
84 | Protocol_error of string
85 | Timeout
86 | Invalid_response of string
87 | Too_many_redirects
88
89type response = {
90 status : int;
91 headers : (string * string) list;
92 body : string;
93 protocol : protocol;
94}
95
96type t = { config : config; h1_client : H1_client.t; h2_client : H2_client.t }
97
98let h1_status_to_int status = H1.Status.to_code status
99let h2_status_to_int status = H2.Status.to_code status
100
101let h1_headers_to_list headers =
102 let result = ref [] in
103 H1.Headers.iter
104 ~f:(fun name value -> result := (name, value) :: !result)
105 headers;
106 List.rev !result
107
108let h2_headers_to_list headers =
109 let result = ref [] in
110 H2.Headers.iter
111 ~f:(fun name value -> result := (name, value) :: !result)
112 headers;
113 List.rev !result
114
115let create ~sw ~net ~clock ?(config = default_config) () =
116 let h1_config : H1_client.config =
117 {
118 connect_timeout = config.connect_timeout;
119 read_timeout = config.read_timeout;
120 write_timeout = config.write_timeout;
121 follow_redirects = config.follow_redirects;
122 buffer_size = config.buffer_size;
123 max_response_body = config.max_response_body;
124 tls = config.tls;
125 default_headers = config.default_headers;
126 pool = config.pool;
127 }
128 in
129 let h2_config : H2_client.config =
130 {
131 connect_timeout = config.connect_timeout;
132 read_timeout = config.read_timeout;
133 buffer_size = config.buffer_size;
134 default_headers = config.default_headers;
135 pool = config.pool;
136 }
137 in
138 {
139 config;
140 h1_client = H1_client.create ~sw ~net ~clock ~config:h1_config ();
141 h2_client = H2_client.create ~sw ~net ~clock ~config:h2_config ();
142 }
143
144let close t =
145 H1_client.close t.h1_client;
146 H2_client.close t.h2_client
147
148let should_use_h2 config url =
149 let uri = Uri.of_string url in
150 let scheme = Uri.scheme uri |> Option.value ~default:"http" in
151 let is_https = String.equal scheme "https" in
152 match config.preferred_protocol with
153 | Some HTTP_2 -> true
154 | Some HTTP_1_1 -> false
155 | None -> (
156 is_https
157 &&
158 match config.tls.alpn_protocols with
159 | Some protos -> List.mem Tls_config.alpn_h2 protos
160 | None -> false)
161
162let map_h1_error = function
163 | H1_client.Connection_failed msg -> Connection_failed msg
164 | H1_client.Tls_error msg -> Tls_error msg
165 | H1_client.Timeout -> Timeout
166 | H1_client.Invalid_response msg -> Invalid_response msg
167 | H1_client.Too_many_redirects -> Too_many_redirects
168
169let map_h2_error = function
170 | H2_client.Connection_failed msg -> Connection_failed msg
171 | H2_client.Tls_error msg -> Tls_error msg
172 | H2_client.Protocol_error msg -> Protocol_error msg
173 | H2_client.Timeout -> Timeout
174 | H2_client.Invalid_response msg -> Invalid_response msg
175
176let request t url =
177 if should_use_h2 t.config url then
178 match H2_client.get t.h2_client url with
179 | Ok resp ->
180 Ok
181 {
182 status = h2_status_to_int resp.H2_client.status;
183 headers = h2_headers_to_list resp.headers;
184 body = resp.body;
185 protocol = HTTP_2;
186 }
187 | Error e -> Error (map_h2_error e)
188 else
189 match H1_client.get t.h1_client url with
190 | Ok resp ->
191 Ok
192 {
193 status = h1_status_to_int resp.H1_client.status;
194 headers = h1_headers_to_list resp.headers;
195 body = resp.body;
196 protocol = HTTP_1_1;
197 }
198 | Error e -> Error (map_h1_error e)
199
200let request_post t url ~body:request_body =
201 if should_use_h2 t.config url then
202 match H2_client.post t.h2_client url ~body:request_body with
203 | Ok resp ->
204 Ok
205 {
206 status = h2_status_to_int resp.H2_client.status;
207 headers = h2_headers_to_list resp.headers;
208 body = resp.body;
209 protocol = HTTP_2;
210 }
211 | Error e -> Error (map_h2_error e)
212 else
213 match H1_client.post t.h1_client url ~body:request_body with
214 | Ok resp ->
215 Ok
216 {
217 status = h1_status_to_int resp.H1_client.status;
218 headers = h1_headers_to_list resp.headers;
219 body = resp.body;
220 protocol = HTTP_1_1;
221 }
222 | Error e -> Error (map_h1_error e)
223
224let get ~sw ~net ~clock ?(config = default_config) url =
225 let t = create ~sw ~net ~clock ~config () in
226 let result = request t url in
227 close t;
228 result
229
230let post ~sw ~net ~clock ?(config = default_config) url ~body =
231 let t = create ~sw ~net ~clock ~config () in
232 let result = request_post t url ~body in
233 close t;
234 result