Created
July 28, 2026 15:44
-
-
Save favila/6e9f56a0e365c7c3e3f767e4e1fc9ca5 to your computer and use it in GitHub Desktop.
Little script to explore what java.net.HttpClient's HttpResponse headers look like for arbitrary http responses (e.g. splitting headers, optional whitespace, etc). Generated by claude code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (import '[java.net InetSocketAddress ServerSocket URI] | |
| '[java.io BufferedReader InputStreamReader] | |
| '[java.nio.charset StandardCharsets] | |
| '[java.net.http HttpClient HttpRequest HttpResponse HttpResponse$BodyHandlers]) | |
| (defn- drain-request! | |
| "Reads the request line + headers off the socket up to the blank line that | |
| terminates them (body ignored -- fine for a GET-only test harness)." | |
| [in] | |
| (let [reader (BufferedReader. (InputStreamReader. in StandardCharsets/US_ASCII))] | |
| (loop [] | |
| (let [line (.readLine reader)] | |
| (when (and line (not (.isEmpty line))) | |
| (recur)))))) | |
| (defn- render-headers | |
| "headers is a seq of [name value] pairs. Order, duplicate names, and any | |
| whitespace embedded in the value string are written verbatim -- nothing | |
| here trims or normalizes anything." | |
| [headers] | |
| (apply str (map (fn [[k v]] (str k ": " v "\r\n")) headers))) | |
| (defn- raw-response-bytes ^bytes [status reason headers ^String body] | |
| (let [body-bytes (.getBytes body StandardCharsets/UTF_8) | |
| has-cl? (some (fn [[k _]] (= "content-length" (.toLowerCase ^String k))) headers) | |
| headers (if has-cl? headers (conj (vec headers) ["Content-Length" (str (count body-bytes))])) | |
| text (str "HTTP/1.1 " status " " reason "\r\n" | |
| (render-headers headers) | |
| "\r\n")] | |
| (byte-array (concat (.getBytes text StandardCharsets/UTF_8) (seq body-bytes))))) | |
| (defn parse-http-response | |
| "headers: a seq of [name value] string pairs. | |
| - repeated header: include the same name twice, e.g. | |
| [[\"Set-Cookie\" \"a=1\"] [\"Set-Cookie\" \"b=2\"]] | |
| - extra optional whitespace (OWS): embed it directly in the value, e.g. | |
| [\"X-Foo\" \" padded-value \"] | |
| Everything is written to the wire byte-for-byte over a one-shot loopback | |
| TCP connection, and the returned HttpResponse<String> is exactly what | |
| java.net.http.HttpClient parsed from those bytes -- including any | |
| normalization (e.g. OWS trimming) the client itself performs." | |
| ([body] (parse-http-response body 200 "OK" [])) | |
| ([body status] (parse-http-response body status "OK" [])) | |
| ([body status reason headers] | |
| (with-open [server (doto (ServerSocket.) | |
| (.bind (InetSocketAddress. "localhost" 0)))] | |
| (let [port (.getLocalPort server) | |
| response-bytes (raw-response-bytes status reason headers body) | |
| server-thread (Thread. | |
| (fn [] | |
| (with-open [sock (.accept server)] | |
| (drain-request! (.getInputStream sock)) | |
| (doto (.getOutputStream sock) | |
| (.write response-bytes) | |
| (.flush))))) | |
| client (HttpClient/newHttpClient) | |
| request (-> (HttpRequest/newBuilder) | |
| (.uri (URI. (str "http://localhost:" port "/"))) | |
| (.GET) | |
| (.build))] | |
| (.start server-thread) | |
| (let [resp (.send client request (HttpResponse$BodyHandlers/ofString))] | |
| (.join server-thread) | |
| resp))))) | |
| (comment | |
| (let [resp (parse-http-response "hello" 200 | |
| [["Content-Encoding" " "] | |
| ["Content-Encoding" " gzip, brotli "] | |
| ])] | |
| (.statusCode resp) ;=> 200 | |
| (.body resp) ;=> "{\"hello\":\"world\"}" | |
| (-> resp .headers (.allValues "content-encoding")) | |
| ) ;=> real HttpHeaders instance | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment