(*--------------------------------------------------------------------------- Copyright (c) 2025 Anil Madhavapeddy . All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) (** HTTP headers management with case-insensitive keys This module provides an efficient implementation of HTTP headers with case-insensitive header names as per RFC 7230. Headers can have multiple values for the same key (e.g., multiple Set-Cookie headers). {2 Examples} {[ let headers = Headers.empty |> Headers.content_type Mime.json |> Headers.bearer "token123" |> Headers.set "X-Custom" "value" ]} *) (** Log source for header operations *) val src : Logs.Src.t type t (** Abstract header collection type. Headers are stored with case-insensitive keys and maintain insertion order. *) (** {1 Creation and Conversion} *) val empty : t (** [empty] creates an empty header collection. *) val of_list : (string * string) list -> t (** [of_list pairs] creates headers from an association list. Later entries override earlier ones for the same key. *) val to_list : t -> (string * string) list (** [to_list headers] converts headers to an association list. The order of headers is preserved. *) (** {1 Header Injection Prevention} *) exception Invalid_header of { name: string; reason: string } (** Raised when a header name or value contains invalid characters (CR/LF) that could enable HTTP request smuggling attacks. *) (** {1 Manipulation} *) val add : string -> string -> t -> t (** [add name value headers] adds a header value. Multiple values for the same header name are allowed (e.g., for Set-Cookie). @raise Invalid_header if the header name or value contains CR/LF characters (to prevent HTTP header injection attacks). *) val set : string -> string -> t -> t (** [set name value headers] sets a header value, replacing any existing values for that header name. @raise Invalid_header if the header name or value contains CR/LF characters (to prevent HTTP header injection attacks). *) val get : string -> t -> string option (** [get name headers] returns the first value for a header name, or [None] if the header doesn't exist. *) val get_all : string -> t -> string list (** [get_all name headers] returns all values for a header name. Returns an empty list if the header doesn't exist. *) val remove : string -> t -> t (** [remove name headers] removes all values for a header name. *) val mem : string -> t -> bool (** [mem name headers] checks if a header name exists. *) val merge : t -> t -> t (** [merge base override] merges two header collections. Headers from [override] replace those in [base]. *) (** {1 Common Header Builders} Convenience functions for setting common HTTP headers. *) val content_type : Mime.t -> t -> t (** [content_type mime headers] sets the Content-Type header. *) val content_length : int64 -> t -> t (** [content_length length headers] sets the Content-Length header. *) val accept : Mime.t -> t -> t (** [accept mime headers] sets the Accept header. *) val authorization : string -> t -> t (** [authorization value headers] sets the Authorization header with a raw value. *) val bearer : string -> t -> t (** [bearer token headers] sets the Authorization header with a Bearer token. Example: [bearer "abc123"] sets ["Authorization: Bearer abc123"] *) val basic : username:string -> password:string -> t -> t (** [basic ~username ~password headers] sets the Authorization header with HTTP Basic authentication (base64-encoded username:password). *) val user_agent : string -> t -> t (** [user_agent ua headers] sets the User-Agent header. *) val host : string -> t -> t (** [host hostname headers] sets the Host header. *) val cookie : string -> string -> t -> t (** [cookie name value headers] adds a cookie to the Cookie header. Multiple cookies can be added by calling this function multiple times. *) val range : start:int64 -> ?end_:int64 -> unit -> t -> t (** [range ~start ?end_ () headers] sets the Range header for partial content. Example: [range ~start:0L ~end_:999L ()] requests the first 1000 bytes. *) (** {1 HTTP 100-Continue Support} Per Recommendation #7: Expect: 100-continue protocol for large uploads. RFC 9110 Section 10.1.1 (Expect) *) val expect : string -> t -> t (** [expect value headers] sets the Expect header. Example: [expect "100-continue"] for large request bodies. *) val expect_100_continue : t -> t (** [expect_100_continue headers] sets [Expect: 100-continue]. Use this for large uploads to allow the server to reject the request before the body is sent, saving bandwidth. *) (** {1 Cache Control Headers} Per Recommendation #17 and #19: Response caching and conditional requests. RFC 9111 (HTTP Caching), RFC 9110 Section 8.8.2-8.8.3 (Last-Modified, ETag) *) val if_none_match : string -> t -> t (** [if_none_match etag headers] sets the If-None-Match header for conditional requests. The request succeeds only if the resource's ETag does NOT match. Used with GET/HEAD to implement efficient caching (returns 304 Not Modified if matches). *) val if_match : string -> t -> t (** [if_match etag headers] sets the If-Match header for conditional requests. The request succeeds only if the resource's ETag matches. Used with PUT/DELETE for optimistic concurrency (prevents lost updates). *) val if_modified_since : string -> t -> t (** [if_modified_since date headers] sets the If-Modified-Since header. The date should be in HTTP-date format (RFC 9110 Section 5.6.7). Example: ["Sun, 06 Nov 1994 08:49:37 GMT"] *) val if_unmodified_since : string -> t -> t (** [if_unmodified_since date headers] sets the If-Unmodified-Since header. The request succeeds only if the resource has NOT been modified since the date. *) val http_date_of_ptime : Ptime.t -> string (** [http_date_of_ptime time] formats a Ptime.t as an HTTP-date. Format: "Sun, 06 Nov 1994 08:49:37 GMT" (RFC 9110 Section 5.6.7) *) val if_modified_since_ptime : Ptime.t -> t -> t (** [if_modified_since_ptime time headers] sets If-Modified-Since using a Ptime.t value. *) val if_unmodified_since_ptime : Ptime.t -> t -> t (** [if_unmodified_since_ptime time headers] sets If-Unmodified-Since using a Ptime.t value. *) val cache_control : string -> t -> t (** [cache_control directives headers] sets the Cache-Control header with a raw directive string. Example: [cache_control "no-cache, max-age=3600"] *) val cache_control_directives : ?max_age:int -> ?max_stale:int option option -> ?min_fresh:int -> ?no_cache:bool -> ?no_store:bool -> ?no_transform:bool -> ?only_if_cached:bool -> unit -> t -> t (** [cache_control_directives ?max_age ?max_stale ?min_fresh ~no_cache ~no_store ~no_transform ~only_if_cached () headers] builds a Cache-Control header from individual directives (RFC 9111 request directives). - [max_age]: Maximum age in seconds the client is willing to accept - [max_stale]: Accept stale responses: - [None]: omit max_stale entirely - [Some None]: "max-stale" (accept any staleness) - [Some (Some n)]: "max-stale=N" (accept n seconds staleness) - [min_fresh]: Response must be fresh for at least n more seconds - [no_cache]: Force revalidation with origin server - [no_store]: Response must not be stored in cache - [no_transform]: Intermediaries must not transform the response - [only_if_cached]: Only return cached response, 504 if not available *) val etag : string -> t -> t (** [etag value headers] sets the ETag header (for responses). Example: [etag "\"abc123\""] *) val last_modified : string -> t -> t (** [last_modified date headers] sets the Last-Modified header (for responses). The date should be in HTTP-date format. *) val last_modified_ptime : Ptime.t -> t -> t (** [last_modified_ptime time headers] sets Last-Modified using a Ptime.t value. *) (** {1 Aliases} *) val get_multi : string -> t -> string list (** [get_multi] is an alias for {!get_all}. *) (** Pretty printer for headers *) val pp : Format.formatter -> t -> unit (** Brief pretty printer showing count only *) val pp_brief : Format.formatter -> t -> unit