A batteries included HTTP/1.1 client in OCaml
at main 253 lines 10 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** HTTP Cache-Control header parsing per RFC 9111 (HTTP Caching) 7 8 This module provides parsing and representation of Cache-Control directives 9 for both requests and responses. It supports all standard directives from 10 RFC 9111 Section 5.2. 11 12 Per Recommendation #17: Response Caching with RFC 7234/9111 Compliance 13 14 {2 Examples} 15 16 {[ 17 (* Parse response Cache-Control *) 18 let cc = Cache_control.parse_response "max-age=3600, public" in 19 Printf.printf "Max age: %d\n" (Option.get cc.max_age); 20 21 (* Check if cacheable *) 22 if Cache_control.is_cacheable ~response_cc:cc ~status:200 then 23 Printf.printf "Response is cacheable\n" 24 ]} *) 25 26val src : Logs.Src.t 27(** Log source for cache control operations. *) 28 29(** {1 Response Cache-Control Directives} 30 31 RFC 9111 Section 5.2.2: Cache-Control Response Directives *) 32 33type response_directive = 34 | Max_age of int (** max-age=N - response is fresh for N seconds *) 35 | S_maxage of int (** s-maxage=N - shared cache max-age *) 36 | No_cache of string list (** no-cache[=headers] - must revalidate *) 37 | No_store (** no-store - must not be stored *) 38 | No_transform (** no-transform - must not be transformed *) 39 | Must_revalidate (** must-revalidate - stale must be revalidated *) 40 | Proxy_revalidate (** proxy-revalidate - shared caches must revalidate *) 41 | Must_understand (** must-understand - RFC 9111 *) 42 | Private of string list (** private[=headers] - only private cache *) 43 | Public (** public - can be stored by any cache *) 44 | Immutable (** immutable - will not change during freshness *) 45 | Stale_while_revalidate of int (** stale-while-revalidate=N *) 46 | Stale_if_error of int (** stale-if-error=N *) 47 | Response_extension of string * string option (** Unknown directive *) 48 49(** {1 Request Cache-Control Directives} 50 51 RFC 9111 Section 5.2.1: Cache-Control Request Directives *) 52 53type request_directive = 54 | Req_max_age of int (** max-age=N *) 55 | Req_max_stale of int option (** max-stale[=N] *) 56 | Req_min_fresh of int (** min-fresh=N *) 57 | Req_no_cache (** no-cache *) 58 | Req_no_store (** no-store *) 59 | Req_no_transform (** no-transform *) 60 | Req_only_if_cached (** only-if-cached *) 61 | Request_extension of string * string option (** Unknown directive *) 62 63(** {1 Parsed Cache-Control Types} *) 64 65type response = { 66 max_age : int option; (** max-age directive value in seconds *) 67 s_maxage : int option; (** s-maxage directive value for shared caches *) 68 no_cache : string list option; 69 (** [None] = not present, [Some []] = present without headers, 70 [Some headers] = must revalidate for these headers *) 71 no_store : bool; (** If true, the response must not be stored *) 72 no_transform : bool; 73 (** If true, intermediaries must not transform the response *) 74 must_revalidate : bool; (** If true, stale responses must be revalidated *) 75 proxy_revalidate : bool; 76 (** Like must_revalidate but only for shared caches *) 77 must_understand : bool; 78 (** If true, cache must understand the caching rules *) 79 private_ : string list option; 80 (** [None] = not present, [Some []] = entirely private, [Some headers] = 81 these headers are private *) 82 public : bool; (** If true, response may be stored by any cache *) 83 immutable : bool; 84 (** If true, response will not change during freshness lifetime *) 85 stale_while_revalidate : int option; 86 (** Seconds stale responses may be served while revalidating *) 87 stale_if_error : int option; 88 (** Seconds stale responses may be served on error *) 89 extensions : (string * string option) list; 90 (** Unknown directives for forward compatibility *) 91} 92(** Parsed response Cache-Control header *) 93 94type request = { 95 req_max_age : int option; 96 (** max-age directive - maximum age client will accept *) 97 req_max_stale : int option option; 98 (** [None] = not present, [Some None] = accept any stale, [Some (Some n)] 99 = accept stale up to n seconds *) 100 req_min_fresh : int option; 101 (** min-fresh directive - response must be fresh for at least n more 102 seconds *) 103 req_no_cache : bool; (** If true, force revalidation with origin server *) 104 req_no_store : bool; (** If true, response must not be stored *) 105 req_no_transform : bool; (** If true, intermediaries must not transform *) 106 req_only_if_cached : bool; 107 (** If true, return cached response or 504 Gateway Timeout *) 108 req_extensions : (string * string option) list; 109 (** Unknown directives for forward compatibility *) 110} 111(** Parsed request Cache-Control header *) 112 113(** {1 Empty Values} *) 114 115val empty_response : response 116(** An empty response Cache-Control (no directives set). *) 117 118val empty_request : request 119(** An empty request Cache-Control (no directives set). *) 120 121(** {1 Parsing Functions} *) 122 123val parse_response : string -> response 124(** [parse_response header_value] parses a response Cache-Control header value. 125 Unknown directives are preserved in [extensions] for forward compatibility. 126*) 127 128val parse_request : string -> request 129(** [parse_request header_value] parses a request Cache-Control header value. 130 Unknown directives are preserved in [req_extensions] for forward 131 compatibility. *) 132 133(** {1 Freshness Calculation} 134 135 RFC 9111 Section 4.2: Freshness *) 136 137val freshness_lifetime : 138 response_cc:response -> ?expires:string -> ?date:string -> unit -> int option 139(** [freshness_lifetime ~response_cc ?expires ?date ()] calculates the freshness 140 lifetime of a response in seconds, based on Cache-Control directives and 141 optional Expires/Date headers. 142 143 Priority (per RFC 9111 Section 4.2.1): 1. max-age directive 2. Expires 144 header minus Date header 3. Returns [None] if no explicit freshness (caller 145 should use heuristics). 146 147 @param response_cc Parsed Cache-Control from response. 148 @param expires Optional Expires header value (HTTP-date format). 149 @param date Optional Date header value (HTTP-date format). *) 150 151(** {1 Age Calculation} 152 153 Per RFC 9111 Section 4.2.3: Calculating Age. *) 154 155type age_inputs = { 156 date_value : Ptime.t option; 157 (** Value of Date header (when response was generated) *) 158 age_value : int; (** Value of Age header in seconds (0 if not present) *) 159 request_time : Ptime.t; (** Time when the request was initiated *) 160 response_time : Ptime.t; (** Time when the response was received *) 161} 162(** Inputs required for age calculation per RFC 9111 Section 4.2.3. *) 163 164val calculate_age : inputs:age_inputs -> now:Ptime.t -> int 165(** [calculate_age ~inputs ~now] calculates the current age of a cached 166 response. 167 168 Per RFC 9111 Section 4.2.3: 169 {v 170 apparent_age = max(0, response_time - date_value) 171 response_delay = response_time - request_time 172 corrected_age_value = age_value + response_delay 173 corrected_initial_age = max(apparent_age, corrected_age_value) 174 resident_time = now - response_time 175 current_age = corrected_initial_age + resident_time 176 v} 177 178 @return Current age in seconds. *) 179 180(** {1 Heuristic Freshness} 181 182 Per RFC 9111 Section 4.2.2: Calculating Heuristic Freshness. *) 183 184val default_heuristic_fraction : float 185(** Default heuristic fraction: 10% of time since Last-Modified. RFC 9111 186 recommends this as a typical value. *) 187 188val default_max_heuristic_age : int 189(** Maximum heuristic freshness lifetime: 1 day (86400 seconds). *) 190 191val heuristic_freshness : 192 ?last_modified:string -> 193 response_time:Ptime.t -> 194 ?fraction:float -> 195 ?max_age:int -> 196 unit -> 197 int option 198(** [heuristic_freshness ?last_modified ~response_time ?fraction ?max_age ()] 199 calculates heuristic freshness lifetime when no explicit caching info 200 provided. 201 202 Per RFC 9111 Section 4.2.2, caches MAY use heuristics when explicit 203 freshness is not available. The typical heuristic is 10% of time since 204 Last-Modified. 205 206 @param last_modified Value of Last-Modified header 207 @param response_time When the response was received 208 @param fraction Fraction of (now - last_modified) to use (default 10%) 209 @param max_age Maximum heuristic age in seconds (default 1 day) 210 @return Heuristic freshness lifetime in seconds, or None. *) 211 212val is_fresh : current_age:int -> freshness_lifetime:int -> bool 213(** [is_fresh ~current_age ~freshness_lifetime] returns true if a cached 214 response is still fresh (current_age < freshness_lifetime). *) 215 216val can_serve_stale : 217 request_cc:request -> current_age:int -> freshness_lifetime:int -> bool 218(** [can_serve_stale ~request_cc ~current_age ~freshness_lifetime] returns true 219 if a stale response can still be served based on request Cache-Control 220 directives (specifically max-stale). *) 221 222(** {1 Cacheability Checks} *) 223 224val is_cacheable : response_cc:response -> status:int -> bool 225(** [is_cacheable ~response_cc ~status] returns true if the response may be 226 cached based on its Cache-Control directives and HTTP status code. 227 228 A response is cacheable if: 229 - no-store is NOT present 230 - Status is cacheable by default (200, 203, 204, 206, 300, 301, 308, 404, 231 405, 410, 414, 501) OR explicit caching directive is present. *) 232 233val must_revalidate : response_cc:response -> bool 234(** [must_revalidate ~response_cc] returns true if cached response must be 235 revalidated with the origin server before use. 236 237 True if any of: must-revalidate, proxy-revalidate, or no-cache is set. *) 238 239val is_public : response_cc:response -> bool 240(** [is_public ~response_cc] returns true if the response may be stored in 241 shared caches (CDNs, proxies). *) 242 243val is_private : response_cc:response -> bool 244(** [is_private ~response_cc] returns true if the response may only be stored in 245 private caches (browser cache). *) 246 247(** {1 Pretty Printers} *) 248 249val pp_response : Format.formatter -> response -> unit 250(** Pretty print a parsed response Cache-Control. *) 251 252val pp_request : Format.formatter -> request -> unit 253(** Pretty print a parsed request Cache-Control. *)