From ad2070f86269f70abd819758d69e90ebbfc4ef10 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 14:04:08 +1200 Subject: [PATCH 1/2] Parse cookie header pairs. --- lib/protocol/http/header/cookie.rb | 7 ++++-- test/protocol/http/header/cookie.rb | 37 ++++++++++------------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/lib/protocol/http/header/cookie.rb b/lib/protocol/http/header/cookie.rb index adc441a..e4718ac 100644 --- a/lib/protocol/http/header/cookie.rb +++ b/lib/protocol/http/header/cookie.rb @@ -49,8 +49,11 @@ def initialize(value = nil) # # @returns [Hash(String, HTTP::Cookie)] a hash where keys are cookie names and values are {HTTP::Cookie} objects. def to_h - cookies = self.collect do |string| - HTTP::Cookie.parse(string) + cookies = self.flat_map do |string| + # Each header field can contain multiple cookie pairs separated by semicolons: + string.split(/\s*;\s*/).map do |pair| + HTTP::Cookie.parse(pair) + end end cookies.map{|cookie| [cookie.name, cookie]}.to_h diff --git a/test/protocol/http/header/cookie.rb b/test/protocol/http/header/cookie.rb index d27a3ed..e780261 100644 --- a/test/protocol/http/header/cookie.rb +++ b/test/protocol/http/header/cookie.rb @@ -16,40 +16,23 @@ expect(header).to be == ["session=abc123"] end - with "session=123; secure" do + with "session=123; user_id=42" do it "can parse cookies" do - expect(cookies).to have_keys("session") + expect(cookies).to have_keys("session", "user_id") session = cookies["session"] expect(session).to have_attributes( name: be == "session", value: be == "123", + directives: be == {}, ) - expect(session.directives).to have_keys("secure") - end - end - - with "session=123; path=/; secure" do - it "can parse cookies" do - session = cookies["session"] - expect(session).to have_attributes( - name: be == "session", - value: be == "123", - directives: be == {"path" => "/", "secure" => true}, - ) - end - end - - with "session=abc123; secure" do - it "can parse cookies" do - expect(cookies).to have_keys("session") - session = cookies["session"] - expect(session).to have_attributes( - name: be == "session", - value: be == "abc123", + user_id = cookies["user_id"] + expect(user_id).to have_attributes( + name: be == "user_id", + value: be == "42", + directives: be == {}, ) - expect(session.directives).to have_keys("secure") end end @@ -65,5 +48,9 @@ it "joins cookies with semicolons and spaces per RFC 6265" do expect(header.to_s).to be == "session=abc123; user_id=42; token=xyz789" end + + it "parses cookies from multiple header fields" do + expect(cookies).to have_keys("session", "user_id", "token") + end end end From 27edf977dddbbfa0ff23ae02fd36c9362d86ccf8 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 14:09:43 +1200 Subject: [PATCH 2/2] Expand cookie header coverage. --- test/protocol/http/header/cookie.rb | 40 ++++++++++++++++++++++ test/protocol/http/header/set_cookie.rb | 44 ++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/test/protocol/http/header/cookie.rb b/test/protocol/http/header/cookie.rb index e780261..c4baf93 100644 --- a/test/protocol/http/header/cookie.rb +++ b/test/protocol/http/header/cookie.rb @@ -5,6 +5,7 @@ # Copyright, 2022, by Herrick Fang. require "protocol/http/header/cookie" +require "protocol/http/headers" describe Protocol::HTTP::Header::Cookie do let(:header) {subject.parse(description)} @@ -36,6 +37,29 @@ end end + with "empty=; token=abc==" do + it "preserves empty values and equals signs" do + expect(cookies["empty"].value).to be == "" + expect(cookies["token"].value).to be == "abc==" + end + end + + with "first=1 ; second=2;\tthird=3" do + it "ignores whitespace around separators" do + expect(cookies.transform_values(&:value)).to be == { + "first" => "1", + "second" => "2", + "third" => "3", + } + end + end + + with "session=first; session=second" do + it "uses the last value for duplicate names" do + expect(cookies["session"].value).to be == "second" + end + end + with "multiple cookies" do let(:header) do cookie = subject.new @@ -53,4 +77,20 @@ expect(cookies).to have_keys("session", "user_id", "token") end end + + it "parses cookies through protocol headers" do + headers = Protocol::HTTP::Headers[[ + ["cookie", "session=abc123; user_id=42"], + ["cookie", "token=xyz789"], + ]] + + header = headers["cookie"] + + expect(header).to be_a(subject) + expect(header.to_h.transform_values(&:value)).to be == { + "session" => "abc123", + "user_id" => "42", + "token" => "xyz789", + } + end end diff --git a/test/protocol/http/header/set_cookie.rb b/test/protocol/http/header/set_cookie.rb index 103b596..8148e14 100644 --- a/test/protocol/http/header/set_cookie.rb +++ b/test/protocol/http/header/set_cookie.rb @@ -4,6 +4,7 @@ # Copyright, 2026, by Samuel Williams. require "protocol/http/header/set_cookie" +require "protocol/http/headers" describe Protocol::HTTP::Header::SetCookie do let(:header) do @@ -19,6 +20,47 @@ end it "can extract parsed cookies" do - expect(header.to_h).to have_keys("session", "theme") + cookies = header.to_h + + expect(cookies).to have_keys("session", "theme") + expect(cookies["session"]).to have_attributes( + value: be == "abc123", + directives: be == {"Path" => "/"}, + ) + expect(cookies["theme"]).to have_attributes( + value: be == "dark", + directives: be == {"HttpOnly" => true}, + ) + end + + it "preserves cookie attributes" do + header = subject.parse("session=abc123; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600; Expires=Wed, 21 Oct 2015 07:28:00 GMT") + cookie = header.to_h["session"] + + expect(cookie.directives).to be == { + "Path" => "/", + "HttpOnly" => true, + "SameSite" => "Lax", + "Max-Age" => "3600", + "Expires" => "Wed, 21 Oct 2015 07:28:00 GMT", + } + end + + it "preserves separate fields through protocol headers" do + headers = Protocol::HTTP::Headers[[ + ["set-cookie", "session=abc123; Path=/; HttpOnly"], + ["set-cookie", "theme=dark; SameSite=Lax"], + ]] + + header = headers["set-cookie"] + cookies = header.to_h + + expect(header).to be_a(subject) + expect(header).to be == [ + "session=abc123; Path=/; HttpOnly", + "theme=dark; SameSite=Lax", + ] + expect(cookies["session"].directives).to be == {"Path" => "/", "HttpOnly" => true} + expect(cookies["theme"].directives).to be == {"SameSite" => "Lax"} end end