Skip to content

Instantly share code, notes, and snippets.

@favila
Created July 28, 2026 15:44
Show Gist options
  • Select an option

  • Save favila/6e9f56a0e365c7c3e3f767e4e1fc9ca5 to your computer and use it in GitHub Desktop.

Select an option

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
(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