ocaml http/1, http/2 and websocket client and server library
1(** Response types and helper functions.
2
3 This module provides the canonical HTTP response type used throughout HCS.
4 It works with both HTTP/1.1 and HTTP/2 protocols. *)
5
6(** {1 Pre-built Responses}
7
8 Pre-built responses for zero-allocation hot paths. Create once at startup,
9 respond many times without allocation. *)
10module Prebuilt = struct
11 type t = {
12 status : H1.Status.t;
13 headers : H1.Headers.t;
14 body : Bigstringaf.t;
15 cached_h1_response : H1.Response.t Atomic.t;
16 cached_second : int Atomic.t;
17 }
18
19 let create ~status ?(headers = []) body_str =
20 let body =
21 Bigstringaf.of_string ~off:0 ~len:(String.length body_str) body_str
22 in
23 let body_len = Bigstringaf.length body in
24 let all_headers = ("content-length", string_of_int body_len) :: headers in
25 let h1_headers = H1.Headers.of_list all_headers in
26 let now = int_of_float (Unix.gettimeofday ()) in
27 let headers_with_date =
28 H1.Headers.add h1_headers "date" (Date_cache.get ())
29 in
30 let cached_resp = H1.Response.create ~headers:headers_with_date status in
31 {
32 status;
33 headers = h1_headers;
34 body;
35 cached_h1_response = Atomic.make cached_resp;
36 cached_second = Atomic.make now;
37 }
38
39 let create_bigstring ~status ?(headers = []) body =
40 let body_len = Bigstringaf.length body in
41 let all_headers = ("content-length", string_of_int body_len) :: headers in
42 let h1_headers = H1.Headers.of_list all_headers in
43 let now = int_of_float (Unix.gettimeofday ()) in
44 let headers_with_date =
45 H1.Headers.add h1_headers "date" (Date_cache.get ())
46 in
47 let cached_resp = H1.Response.create ~headers:headers_with_date status in
48 {
49 status;
50 headers = h1_headers;
51 body;
52 cached_h1_response = Atomic.make cached_resp;
53 cached_second = Atomic.make now;
54 }
55
56 let[@inline] get_cached_h1_response t =
57 let now = int_of_float (Unix.gettimeofday ()) in
58 let last = Atomic.get t.cached_second in
59 if now <> last then begin
60 let headers = H1.Headers.add t.headers "date" (Date_cache.get ()) in
61 let resp = H1.Response.create ~headers t.status in
62 Atomic.set t.cached_h1_response resp;
63 Atomic.set t.cached_second now
64 end;
65 Atomic.get t.cached_h1_response
66
67 let[@inline] respond_h1 reqd t =
68 let response = get_cached_h1_response t in
69 H1.Reqd.respond_with_bigstring reqd response t.body
70
71 let[@inline] respond_h2 reqd t =
72 let result = ref [] in
73 H1.Headers.iter
74 ~f:(fun name value -> result := (name, value) :: !result)
75 t.headers;
76 let h2_headers = H2.Headers.of_list (List.rev !result) in
77 let h2_status = (t.status :> H2.Status.t) in
78 let response = H2.Response.create ~headers:h2_headers h2_status in
79 H2.Reqd.respond_with_bigstring reqd response t.body
80end
81
82(** {1 Types} *)
83
84type body =
85 | Empty
86 | String of string
87 | Cstruct of Cstruct.t
88 | Bigstring of Bigstringaf.t
89 | Stream of { content_length : int64 option; next : unit -> Cstruct.t option }
90 | Prebuilt_body of Prebuilt.t
91
92type t = { status : H1.Status.t; headers : (string * string) list; body : body }
93
94(** {1 Basic Constructors} *)
95
96let make ?(status = `OK) ?(headers = []) body_str : t =
97 { status; headers; body = String body_str }
98
99let empty ?(status = `No_content) ?(headers = []) () : t =
100 { status; headers; body = Empty }
101
102(** {1 Status shortcuts - 2xx Success} *)
103
104let ok ?(headers = []) body : t = { status = `OK; headers; body = String body }
105
106let created ?(headers = []) ?location body : t =
107 let headers =
108 match location with
109 | Some loc -> ("Location", loc) :: headers
110 | None -> headers
111 in
112 { status = `Created; headers; body = String body }
113
114let accepted ?(headers = []) body : t =
115 { status = `Accepted; headers; body = String body }
116
117let no_content ?(headers = []) () : t =
118 { status = `No_content; headers; body = Empty }
119
120(** {1 Status shortcuts - 3xx Redirection} *)
121
122let redirect ?(permanent = false) ?(headers = []) location : t =
123 let status = if permanent then `Moved_permanently else `Found in
124 { status; headers = ("Location", location) :: headers; body = Empty }
125
126let moved_permanently ?(headers = []) location : t =
127 redirect ~permanent:true ~headers location
128
129let found ?(headers = []) location : t =
130 redirect ~permanent:false ~headers location
131
132let see_other ?(headers = []) location : t =
133 {
134 status = `See_other;
135 headers = ("Location", location) :: headers;
136 body = Empty;
137 }
138
139let temporary_redirect ?(headers = []) location : t =
140 {
141 status = `Temporary_redirect;
142 headers = ("Location", location) :: headers;
143 body = Empty;
144 }
145
146let not_modified ?(headers = []) () : t =
147 { status = `Not_modified; headers; body = Empty }
148
149(** {1 Status shortcuts - 4xx Client Errors} *)
150
151let bad_request ?(headers = []) ?(body = "Bad Request") () : t =
152 { status = `Bad_request; headers; body = String body }
153
154let unauthorized ?(headers = []) ?www_authenticate () : t =
155 let headers =
156 match www_authenticate with
157 | Some auth -> ("WWW-Authenticate", auth) :: headers
158 | None -> headers
159 in
160 { status = `Unauthorized; headers; body = String "Unauthorized" }
161
162let forbidden ?(headers = []) ?(body = "Forbidden") () : t =
163 { status = `Forbidden; headers; body = String body }
164
165let not_found ?(headers = []) ?(body = "Not Found") () : t =
166 { status = `Not_found; headers; body = String body }
167
168let method_not_allowed ?(headers = []) ~allowed () : t =
169 let allow_header =
170 String.concat ", " (List.map H1.Method.to_string allowed)
171 in
172 {
173 status = `Method_not_allowed;
174 headers = ("Allow", allow_header) :: headers;
175 body = String "Method Not Allowed";
176 }
177
178let conflict ?(headers = []) ?(body = "Conflict") () : t =
179 { status = `Conflict; headers; body = String body }
180
181let gone ?(headers = []) ?(body = "Gone") () : t =
182 { status = `Gone; headers; body = String body }
183
184let unprocessable_entity ?(headers = []) ?(body = "Unprocessable Entity") () : t
185 =
186 { status = `Code 422; headers; body = String body }
187
188let too_many_requests ?(headers = []) ?retry_after () : t =
189 let headers =
190 match retry_after with
191 | Some secs -> ("Retry-After", string_of_int secs) :: headers
192 | None -> headers
193 in
194 { status = `Code 429; headers; body = String "Too Many Requests" }
195
196(** {1 Status shortcuts - 5xx Server Errors} *)
197
198let internal_error ?(headers = []) ?(body = "Internal Server Error") () : t =
199 { status = `Internal_server_error; headers; body = String body }
200
201let not_implemented ?(headers = []) ?(body = "Not Implemented") () : t =
202 { status = `Not_implemented; headers; body = String body }
203
204let bad_gateway ?(headers = []) ?(body = "Bad Gateway") () : t =
205 { status = `Bad_gateway; headers; body = String body }
206
207let service_unavailable ?(headers = []) ?retry_after () : t =
208 let headers =
209 match retry_after with
210 | Some secs -> ("Retry-After", string_of_int secs) :: headers
211 | None -> headers
212 in
213 {
214 status = `Service_unavailable;
215 headers;
216 body = String "Service Unavailable";
217 }
218
219let gateway_timeout ?(headers = []) ?(body = "Gateway Timeout") () : t =
220 { status = `Gateway_timeout; headers; body = String body }
221
222(** {1 Content-Type helpers} *)
223
224let text ?(status = `OK) body : t =
225 {
226 status;
227 headers = [ ("Content-Type", "text/plain; charset=utf-8") ];
228 body = String body;
229 }
230
231let html ?(status = `OK) body : t =
232 {
233 status;
234 headers = [ ("Content-Type", "text/html; charset=utf-8") ];
235 body = String body;
236 }
237
238let json ?(status = `OK) body : t =
239 {
240 status;
241 headers = [ ("Content-Type", "application/json; charset=utf-8") ];
242 body = String body;
243 }
244
245let xml ?(status = `OK) body : t =
246 {
247 status;
248 headers = [ ("Content-Type", "application/xml; charset=utf-8") ];
249 body = String body;
250 }
251
252(** {1 Streaming and special responses} *)
253
254let stream ?(status = `OK) ?(headers = []) ?content_length next : t =
255 { status; headers; body = Stream { content_length; next } }
256
257let bigstring ?(status = `OK) ?(headers = []) bstr : t =
258 { status; headers; body = Bigstring bstr }
259
260let cstruct ?(status = `OK) ?(headers = []) cs : t =
261 { status; headers; body = Cstruct cs }
262
263let prebuilt p : t =
264 { status = p.Prebuilt.status; headers = []; body = Prebuilt_body p }
265
266(** {1 Response modifiers} *)
267
268let with_header name value (resp : t) : t =
269 { resp with headers = (name, value) :: resp.headers }
270
271let with_headers headers (resp : t) : t =
272 { resp with headers = headers @ resp.headers }
273
274let with_body body_str (resp : t) : t = { resp with body = String body_str }
275let with_body_raw body (resp : t) : t = { resp with body }
276let with_status status (resp : t) : t = { resp with status }
277
278let with_content_type content_type resp =
279 with_header "Content-Type" content_type resp
280
281let with_cache_control directive resp =
282 with_header "Cache-Control" directive resp
283
284let with_no_cache resp =
285 resp
286 |> with_header "Cache-Control" "no-store, no-cache, must-revalidate"
287 |> with_header "Pragma" "no-cache"
288
289let with_cors ?(origin = "*") ?(methods = "GET, POST, PUT, DELETE, OPTIONS")
290 ?(headers = "Content-Type, Authorization") resp =
291 resp
292 |> with_header "Access-Control-Allow-Origin" origin
293 |> with_header "Access-Control-Allow-Methods" methods
294 |> with_header "Access-Control-Allow-Headers" headers
295
296(** {1 Cookie helpers} *)
297
298let with_cookie ?(path = "/") ?(http_only = true) ?(secure = false)
299 ?(same_site = "Lax") ?max_age name value resp =
300 let buf = Buffer.create 128 in
301 Buffer.add_string buf name;
302 Buffer.add_char buf '=';
303 Buffer.add_string buf value;
304 Buffer.add_string buf "; Path=";
305 Buffer.add_string buf path;
306 if http_only then Buffer.add_string buf "; HttpOnly";
307 if secure then Buffer.add_string buf "; Secure";
308 Buffer.add_string buf "; SameSite=";
309 Buffer.add_string buf same_site;
310 (match max_age with
311 | None -> ()
312 | Some age ->
313 Buffer.add_string buf "; Max-Age=";
314 Buffer.add_string buf (string_of_int age));
315 with_header "Set-Cookie" (Buffer.contents buf) resp
316
317let clear_cookie ?(path = "/") name resp =
318 with_cookie ~path ~max_age:0 name "" resp