My aggregated monorepo of OCaml code, automaintained

Add HTTP/2 header validation to request path

Add validate_h2_user_headers function to validate user-provided headers
for HTTP/2 compliance before pseudo-headers are added:
- Rejects pseudo-headers in user input (they should not provide them)
- Validates no uppercase in header names (RFC 9113 Section 8.2)
- Validates no connection-specific headers (RFC 9113 Section 8.2.2)
- Validates TE header only contains "trailers" if present

Call this validation in h2_adapter.ml for both request() and one_request()
functions to ensure HTTP/2 header constraints are enforced.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+58
+34
ocaml-requests/lib/core/headers.ml
··· 617 617 Error Connection_header_forbidden 618 618 else 619 619 Ok () 620 + 621 + let validate_h2_user_headers t = 622 + (* Validate user-provided headers for HTTP/2 (before pseudo-headers are added). 623 + Per RFC 9113 Section 8.2.2 and 8.3, validates: 624 + - No pseudo-headers (user should not provide them) 625 + - No uppercase letters in header names 626 + - No connection-specific headers 627 + - TE header only contains "trailers" if present *) 628 + let headers_list = to_list t in 629 + 630 + (* Check for any pseudo-headers (user should not provide them) *) 631 + let pseudo = List.find_opt (fun (name, _) -> is_pseudo_header name) headers_list in 632 + match pseudo with 633 + | Some (name, _) -> 634 + let name_without_colon = String.sub name 1 (String.length name - 1) in 635 + Error (Invalid_pseudo (name_without_colon ^ " (user-provided headers must not contain pseudo-headers)")) 636 + | None -> 637 + (* Check for uppercase in header names *) 638 + let uppercase_header = List.find_opt (fun (name, _) -> 639 + contains_uppercase name 640 + ) headers_list in 641 + match uppercase_header with 642 + | Some (name, _) -> Error (Uppercase_header_name name) 643 + | None -> 644 + (* Check for forbidden connection-specific headers *) 645 + let has_forbidden = List.exists (fun h -> mem h t) h2_forbidden_headers in 646 + if has_forbidden then 647 + Error Connection_header_forbidden 648 + else 649 + (* Check TE header - only "trailers" is allowed *) 650 + match get `Te t with 651 + | Some te when String.lowercase_ascii (String.trim te) <> "trailers" -> 652 + Error Te_header_invalid 653 + | _ -> Ok ()
+12
ocaml-requests/lib/core/headers.mli
··· 453 453 - No uppercase letters in header names 454 454 - No connection-specific headers *) 455 455 456 + val validate_h2_user_headers : t -> (unit, h2_validation_error) result 457 + (** [validate_h2_user_headers headers] validates user-provided headers for HTTP/2. 458 + 459 + Unlike {!validate_h2_request}, this validates headers {i before} pseudo-headers 460 + are added by the HTTP/2 layer. Use this in the HTTP adapter. 461 + 462 + Per RFC 9113 Section 8.2.2 and 8.3, validates: 463 + - No pseudo-headers (user should not provide them) 464 + - No uppercase letters in header names 465 + - No connection-specific headers (Connection, Keep-Alive, etc.) 466 + - TE header only contains "trailers" if present *) 467 + 456 468 (** {2 HTTP/2 Forbidden Headers} 457 469 458 470 Per RFC 9113 Section 8.2.2, certain headers are connection-specific
+12
ocaml-requests/lib/h2/h2_adapter.ml
··· 136 136 ~auto_decompress 137 137 () 138 138 : (response, string) result = 139 + (* Validate HTTP/2 header constraints per RFC 9113 Section 8.2.2 and 8.3 *) 140 + match Headers.validate_h2_user_headers headers with 141 + | Error e -> 142 + Error (Format.asprintf "Invalid HTTP/2 request headers: %a" 143 + Headers.pp_h2_validation_error e) 144 + | Ok () -> 139 145 let host = Uri.host uri |> Option.value ~default:"" in 140 146 let port = Uri.port uri |> Option.value ~default:443 in 141 147 let h2_headers = headers_to_h2 headers in ··· 167 173 ~auto_decompress 168 174 () 169 175 : (response, string) result = 176 + (* Validate HTTP/2 header constraints per RFC 9113 Section 8.2.2 and 8.3 *) 177 + match Headers.validate_h2_user_headers headers with 178 + | Error e -> 179 + Error (Format.asprintf "Invalid HTTP/2 request headers: %a" 180 + Headers.pp_h2_validation_error e) 181 + | Ok () -> 170 182 let h2_headers = headers_to_h2 headers in 171 183 let h2_body = Option.bind body body_to_string_opt in 172 184 let meth = Method.to_string method_ in