Skip to content

Commit 3523e1c

Browse files
andrehjrandrew
authored andcommitted
Fix cookie header duplication (#522)
* Add failing spec for multiple Set-Cookie headers bug * When ActionDispatch::Cookies is available from Rails tries to use it. Otherwise parse current cookies set on response and replace it using Rack directly. * Keep compatibility with Rack ~> 1.6. New methods like Rack::Utils#make_delete_cookie_header were only added on 2.0.0
1 parent abb5d64 commit 3523e1c

File tree

2 files changed

+52
-2
lines changed

2 files changed

+52
-2
lines changed

lib/split/persistence/cookie_adapter.rb

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Persistence
66
class CookieAdapter
77

88
def initialize(context)
9+
@context = context
910
@request, @response = context.request, context.response
1011
@cookies = @request.cookies
1112
@expires = Time.now + cookie_length_config
@@ -30,13 +31,40 @@ def keys
3031
private
3132

3233
def set_cookie(value = {})
33-
@response.set_cookie :split.to_s, default_options.merge(value: JSON.generate(value))
34+
cookie_key = :split.to_s
35+
cookie_value = default_options.merge(value: JSON.generate(value))
36+
if action_dispatch?
37+
@context.cookies[cookie_key] = cookie_value
38+
else
39+
set_cookie_via_rack(cookie_key, cookie_value)
40+
end
3441
end
3542

3643
def default_options
3744
{ expires: @expires, path: '/' }
3845
end
3946

47+
def set_cookie_via_rack(key, value)
48+
delete_cookie_header!(@response.header, key, value)
49+
Rack::Utils.set_cookie_header!(@response.header, key, value)
50+
end
51+
52+
# Use Rack::Utils#make_delete_cookie_header after Rack 2.0.0
53+
def delete_cookie_header!(header, key, value)
54+
cookie_header = header['Set-Cookie']
55+
case cookie_header
56+
when nil, ''
57+
cookies = []
58+
when String
59+
cookies = cookie_header.split("\n")
60+
when Array
61+
cookies = cookie_header
62+
end
63+
64+
cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
65+
header['Set-Cookie'] = cookies.join("\n")
66+
end
67+
4068
def hash
4169
@hash ||= begin
4270
if cookies = @cookies[:split.to_s]
@@ -55,6 +83,9 @@ def cookie_length_config
5583
Split.configuration.persistence_cookie_length
5684
end
5785

86+
def action_dispatch?
87+
defined?(Rails) && @response.is_a?(ActionDispatch::Response)
88+
end
5889
end
5990
end
6091
end

spec/persistence/cookie_adapter_spec.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
let(:env) { Rack::MockRequest.env_for("http://example.com:8080/") }
88
let(:request) { Rack::Request.new(env) }
99
let(:response) { Rack::MockResponse.new(200, {}, "") }
10-
let(:context) { double(request: request, response: response) }
10+
let(:context) { double(request: request, response: response, cookies: CookiesMock.new) }
1111
subject { Split::Persistence::CookieAdapter.new(context) }
1212

1313
describe "#[] and #[]=" do
@@ -40,4 +40,23 @@
4040
expect(subject["my_key"]).to eq("my_value")
4141
end
4242

43+
it "puts multiple experiments in a single cookie" do
44+
subject["foo"] = "FOO"
45+
subject["bar"] = "BAR"
46+
expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} -0000\Z/)
47+
end
48+
49+
it "ensure other added cookies are not overriden" do
50+
context.response.set_cookie 'dummy', 'wow'
51+
subject["foo"] = "FOO"
52+
expect(context.response.headers["Set-Cookie"]).to include("dummy=wow")
53+
expect(context.response.headers["Set-Cookie"]).to include("split=")
54+
end
55+
56+
it "uses ActionDispatch::Cookie when available for cookie writing" do
57+
allow(subject).to receive(:action_dispatch?).and_return(true)
58+
subject["foo"] = "FOO"
59+
expect(subject['foo']).to eq('FOO')
60+
end
61+
4362
end

0 commit comments

Comments
 (0)