Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/protocol/http/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,23 @@ module HTTP
# A generic, HTTP protocol error.
class Error < StandardError
end

# Represents a bad request error (as opposed to a server error).
# This is used to indicate that the request was malformed or invalid.
module BadRequest
end

# Raised when a singleton (e.g. `content-length`) header is duplicated in a request or response.
class DuplicateHeaderError < Error
include BadRequest

# @parameter key [String] The header key that was duplicated.
def initialize(key)
super("Duplicate singleton header key: #{key.inspect}")
end

# @attribute [String] key The header key that was duplicated.
attr :key
end
end
end
16 changes: 11 additions & 5 deletions lib/protocol/http/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Released under the MIT License.
# Copyright, 2018-2025, by Samuel Williams.

require_relative "error"

require_relative "header/split"
require_relative "header/multiple"

Expand Down Expand Up @@ -238,16 +240,16 @@ def []= key, value
# The policy for various headers, including how they are merged and normalized.
POLICY = {
# Headers which may only be specified once:
"content-type" => false,
"content-disposition" => false,
"content-length" => false,
"user-agent" => false,
"referer" => false,
"host" => false,
"content-type" => false,
"from" => false,
"host" => false,
"location" => false,
"max-forwards" => false,
"referer" => false,
"retry-after" => false,
"user-agent" => false,

# Custom headers:
"connection" => Header::Connection,
Expand All @@ -267,6 +269,7 @@ def []= key, value
"etag" => Header::ETag,
"if-match" => Header::ETags,
"if-none-match" => Header::ETags,
"if-range" => false,

# Headers which may be specified multiple times, but which can't be concatenated:
"www-authenticate" => Multiple,
Expand Down Expand Up @@ -332,7 +335,10 @@ def delete(key)
hash[key] = policy.new(value)
end
else
# We can't merge these, we only expose the last one set.
if hash.key?(key)
raise DuplicateHeaderError, key
end

hash[key] = value
end
end
Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## Unreleased

- `Protocol::HTTP::Headers` now raise a `DuplicateHeaderError` when a duplicate singleton header (e.g. `content-length`) is added.

## v0.50.0

- Drop support for Ruby v3.1.
Expand Down
19 changes: 15 additions & 4 deletions test/protocol/http/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,33 @@
with "#merge" do
it "should merge headers" do
other = subject[[
# This will replace the original one:
["Content-Type", "text/plain"],

# This will be appended:
["Set-Cookie", "goodbye=world"],
]]

merged = headers.merge(other)

expect(merged.to_h).to be == {
"content-type" => "text/plain",
"content-type" => "text/html",
"set-cookie" => ["hello=world", "foo=bar", "goodbye=world"],
"accept" => ["*/*"],
"connection" => ["keep-alive"]
}
end

it "can't merge singleton headers" do
other = subject[[
["content-type", "text/plain"],
]]

# This doesn't fail as we haven't built an internal index yet:
merged = headers.merge(other)

expect do
# Once we build the index, it will fail:
merged.to_h
end.to raise_exception(Protocol::HTTP::DuplicateHeaderError)
end
end

with "#extract" do
Expand Down
Loading