Skip to content

Commit 2c346ea

Browse files
authored
Raise an error if singleton headers are set multiple times. (#84)
1 parent 69c37f5 commit 2c346ea

File tree

4 files changed

+48
-9
lines changed

4 files changed

+48
-9
lines changed

lib/protocol/http/error.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,23 @@ module HTTP
88
# A generic, HTTP protocol error.
99
class Error < StandardError
1010
end
11+
12+
# Represents a bad request error (as opposed to a server error).
13+
# This is used to indicate that the request was malformed or invalid.
14+
module BadRequest
15+
end
16+
17+
# Raised when a singleton (e.g. `content-length`) header is duplicated in a request or response.
18+
class DuplicateHeaderError < Error
19+
include BadRequest
20+
21+
# @parameter key [String] The header key that was duplicated.
22+
def initialize(key)
23+
super("Duplicate singleton header key: #{key.inspect}")
24+
end
25+
26+
# @attribute [String] key The header key that was duplicated.
27+
attr :key
28+
end
1129
end
1230
end

lib/protocol/http/headers.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Released under the MIT License.
44
# Copyright, 2018-2025, by Samuel Williams.
55

6+
require_relative "error"
7+
68
require_relative "header/split"
79
require_relative "header/multiple"
810

@@ -238,16 +240,16 @@ def []= key, value
238240
# The policy for various headers, including how they are merged and normalized.
239241
POLICY = {
240242
# Headers which may only be specified once:
241-
"content-type" => false,
242243
"content-disposition" => false,
243244
"content-length" => false,
244-
"user-agent" => false,
245-
"referer" => false,
246-
"host" => false,
245+
"content-type" => false,
247246
"from" => false,
247+
"host" => false,
248248
"location" => false,
249249
"max-forwards" => false,
250+
"referer" => false,
250251
"retry-after" => false,
252+
"user-agent" => false,
251253

252254
# Custom headers:
253255
"connection" => Header::Connection,
@@ -267,6 +269,7 @@ def []= key, value
267269
"etag" => Header::ETag,
268270
"if-match" => Header::ETags,
269271
"if-none-match" => Header::ETags,
272+
"if-range" => false,
270273

271274
# Headers which may be specified multiple times, but which can't be concatenated:
272275
"www-authenticate" => Multiple,
@@ -332,7 +335,10 @@ def delete(key)
332335
hash[key] = policy.new(value)
333336
end
334337
else
335-
# We can't merge these, we only expose the last one set.
338+
if hash.key?(key)
339+
raise DuplicateHeaderError, key
340+
end
341+
336342
hash[key] = value
337343
end
338344
end

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- `Protocol::HTTP::Headers` now raise a `DuplicateHeaderError` when a duplicate singleton header (e.g. `content-length`) is added.
6+
37
## v0.50.0
48

59
- Drop support for Ruby v3.1.

test/protocol/http/headers.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,33 @@
4242
with "#merge" do
4343
it "should merge headers" do
4444
other = subject[[
45-
# This will replace the original one:
46-
["Content-Type", "text/plain"],
47-
4845
# This will be appended:
4946
["Set-Cookie", "goodbye=world"],
5047
]]
5148

5249
merged = headers.merge(other)
5350

5451
expect(merged.to_h).to be == {
55-
"content-type" => "text/plain",
52+
"content-type" => "text/html",
5653
"set-cookie" => ["hello=world", "foo=bar", "goodbye=world"],
5754
"accept" => ["*/*"],
5855
"connection" => ["keep-alive"]
5956
}
6057
end
58+
59+
it "can't merge singleton headers" do
60+
other = subject[[
61+
["content-type", "text/plain"],
62+
]]
63+
64+
# This doesn't fail as we haven't built an internal index yet:
65+
merged = headers.merge(other)
66+
67+
expect do
68+
# Once we build the index, it will fail:
69+
merged.to_h
70+
end.to raise_exception(Protocol::HTTP::DuplicateHeaderError)
71+
end
6172
end
6273

6374
with "#extract" do

0 commit comments

Comments
 (0)