Summary
ResponseBuilder iterates response headers with each_slice(2) over an already-paired array. This collapses each header's name and value into a single stringified Ruby array, so every response leaves the server with malformed headers — including a missing/invalid Content-Type.
- File:
lib/responses/response_builder.rb
- Severity: High (affects 100% of responses)
- Type: Correctness bug
What happens
Even with the single Content-Type header that ResponseFactory produces today, the server writes this to the socket:
["content-type", "text/html"]:
The header name becomes the stringified array ["content-type", "text/html"] and the value is empty, so no valid Content-Type is ever sent. With two or more headers, adjacent pairs get merged together and mangled completely.
As a side effect, the exclusion guard is also dead:
next if %w[content-length connection].include?(key.to_s.downcase)
key is now an Array, so key.to_s.downcase never matches and the filter never runs.
Why it slipped through
Browsers MIME-sniff the response body, so the demo (cave.jpg) still renders and looks correct. Any client that trusts Content-Type — API consumers, fetch() reading response.headers, content negotiation — gets the wrong result. Existing specs pass only because Net::HTTP tolerates the malformed output and there is just one header.
Steps to reproduce
- Start the server and request any file (e.g.
GET /cave.jpg).
- Inspect the raw response headers (
curl -i or read the socket directly).
- Observe there is no valid
Content-Type header; instead a malformed line like ["content-type", "image/jpeg"]: is present.
Fix
- response.headers.fields.each_slice(2) do |key, value|
+ response.headers.fields.each do |key, value|
This restores correct header lines and revives the content-length/connection exclusion filter.
Follow-up
Add a spec that reads the raw socket bytes and asserts a literal Content-Type: text/html\r\n line, so the regression is locked out (current
tests pass only because Net::HTTP tolerates the malformed output).
Summary
ResponseBuilderiterates response headers witheach_slice(2)over an already-paired array. This collapses each header's name and value into a single stringified Ruby array, so every response leaves the server with malformed headers — including a missing/invalidContent-Type.lib/responses/response_builder.rbWhat happens
Even with the single
Content-Typeheader thatResponseFactoryproduces today, the server writes this to the socket:The header name becomes the stringified array
["content-type", "text/html"]and the value is empty, so no validContent-Typeis ever sent. With two or more headers, adjacent pairs get merged together and mangled completely.As a side effect, the exclusion guard is also dead:
keyis now anArray, sokey.to_s.downcasenever matches and the filter never runs.Why it slipped through
Browsers MIME-sniff the response body, so the demo (
cave.jpg) still renders and looks correct. Any client that trustsContent-Type— API consumers,fetch()readingresponse.headers, content negotiation — gets the wrong result. Existing specs pass only becauseNet::HTTPtolerates the malformed output and there is just one header.Steps to reproduce
GET /cave.jpg).curl -ior read the socket directly).Content-Typeheader; instead a malformed line like["content-type", "image/jpeg"]:is present.Fix
This restores correct header lines and revives the content-length/connection exclusion filter.
Follow-up
Add a spec that reads the raw socket bytes and asserts a literal
Content-Type: text/html\r\nline, so the regression is locked out (currenttests pass only because
Net::HTTPtolerates the malformed output).