@@ -52,7 +52,12 @@ def run
5252 Host : application . configuration . ip
5353 } ) . merge ( application . configuration . adapter_options )
5454
55- @server = ::Rack ::Server . new ( options )
55+ if rack_v3?
56+ require 'rackup'
57+ @server = ::Rackup ::Server . new ( options )
58+ else
59+ @server = ::Rack ::Server . new ( options )
60+ end
5661 @server . start
5762 end
5863
@@ -70,18 +75,33 @@ def call(env)
7075 response . headers [ SERVER ] = VERSION_STRING
7176
7277 rack_status = response . code
73- rack_headers = response . headers . flattened ( NEWLINE )
78+ rack_headers = build_rack_response_headers ( response . headers )
7479 rack_body = case response . body
7580 when String # Strings are enumerable in ruby 1.8
7681 [ response . body ]
7782 else
7883 if ( io_body = IO . try_convert ( response . body ) )
7984 io_body
8085 elsif response . body . respond_to? ( :call )
81- Webmachine ::ChunkedBody . new ( Array ( response . body . call ) )
86+ if rack_v3?
87+ # In Rack 3 the server (e.g. WEBrick via Rackup) buffers the body
88+ # and applies chunked encoding itself when Transfer-Encoding is set,
89+ # so we must not pre-encode with ChunkedBody.
90+ [ response . body . call ]
91+ else
92+ # Rack 2's WEBrick handler sends the body as-is; ChunkedBody is
93+ # required to produce valid chunked-encoded wire data.
94+ Webmachine ::ChunkedBody . new ( Array ( response . body . call ) )
95+ end
8296 elsif response . body . respond_to? ( :each )
83- # This might be an IOEncoder with a Content-Length, which shouldn't be chunked.
84- if response . headers [ TRANSFER_ENCODING ] == 'chunked'
97+ if rack_v3?
98+ # Return the plain enumerable. Rackup buffers it into a String then
99+ # WEBrick chunks that String when Transfer-Encoding: chunked is set.
100+ response . body
101+ elsif response . headers [ TRANSFER_ENCODING ] == 'chunked'
102+ # Rack 2: only pre-encode bodies that are already marked chunked;
103+ # IOEncoder bodies carry their own Content-Length and must not be
104+ # wrapped.
85105 Webmachine ::ChunkedBody . new ( response . body )
86106 else
87107 response . body
@@ -97,6 +117,45 @@ def call(env)
97117
98118 protected
99119
120+ # Build a Rack-compatible response headers hash from Webmachine's response
121+ # headers.
122+ #
123+ # Header names are always lowercased: this is required by Rack 3 and is
124+ # harmless for Rack 2 (all Rack 2 handlers match header names
125+ # case-insensitively).
126+ #
127+ # The +set-cookie+ value is formatted differently per Rack version:
128+ #
129+ # * Rack 3 / Rackup: the value must be an Array. Rackup's WEBrick handler
130+ # deletes the lowercase +set-cookie+ key and calls
131+ # +res.cookies.concat(Array(value))+, emitting one Set-Cookie line per
132+ # cookie. Joining with +\n+ instead would produce a header value
133+ # containing a newline, which WEBrick 1.9+ rejects as
134+ # +WEBrick::HTTPResponse::InvalidHeader+.
135+ #
136+ # * Rack 2 / Rack::Handler::WEBrick: the handler splits on +\n+ before
137+ # adding cookies (+vs.split("\n")+), so the value must be a newline-joined
138+ # String. Passing an Array causes a +NoMethodError+ because +Array+ does
139+ # not define +#split+.
140+ def build_rack_response_headers ( response_headers )
141+ response_headers . each_with_object ( { } ) do |( key , value ) , h |
142+ rack_key = key . downcase
143+ h [ rack_key ] = if rack_key == 'set-cookie'
144+ if rack_v3?
145+ # Array lets Rackup emit one Set-Cookie header per cookie.
146+ Array ( value )
147+ else
148+ # Rack 2's handler splits on \n; give it a newline-joined String.
149+ Array ( value ) . join ( NEWLINE )
150+ end
151+ elsif value . is_a? ( Array )
152+ value . join ( NEWLINE )
153+ else
154+ value
155+ end
156+ end
157+ end
158+
100159 def routing_tokens ( rack_req )
101160 nil # no-op for default, un-mapped rack adapter
102161 end
@@ -107,6 +166,11 @@ def base_uri(rack_req)
107166
108167 private
109168
169+ # Returns true when running under Rack 3.x.
170+ def rack_v3?
171+ ::Rack . release . start_with? ( '3.' )
172+ end
173+
110174 def build_webmachine_request ( rack_req , headers )
111175 RackRequest . new ( rack_req . request_method ,
112176 rack_req . url ,
@@ -128,6 +192,9 @@ def initialize(method, uri, headers, body, routing_tokens, base_uri, env)
128192
129193 class RackResponse
130194 ONE_FIVE = '1.5' . freeze
195+ # Header names are normalised to lowercase by build_rack_response_headers,
196+ # so use the lowercase form everywhere inside RackResponse too.
197+ LOWERCASE_CONTENT_TYPE = 'content-type' . freeze
131198
132199 def initialize ( body , status , headers )
133200 @body = body
@@ -136,8 +203,8 @@ def initialize(body, status, headers)
136203 end
137204
138205 def finish
139- @headers [ CONTENT_TYPE ] ||= TEXT_HTML if rack_release_enforcing_content_type
140- @headers . delete ( CONTENT_TYPE ) if response_without_body
206+ @headers [ LOWERCASE_CONTENT_TYPE ] ||= TEXT_HTML if rack_release_enforcing_content_type
207+ @headers . delete ( LOWERCASE_CONTENT_TYPE ) if response_without_body
141208 [ @status , @headers , @body ]
142209 end
143210
@@ -177,8 +244,12 @@ def to_s
177244 if @value
178245 @value . join
179246 else
180- @request . body . rewind
181- @request . body . read
247+ # Rack 3 removed the requirement for rack.input to implement #rewind
248+ # (Rack::Lint::InputWrapper in Rack 3 does not define it), so guard
249+ # the call to avoid a NoMethodError on every PUT/POST request.
250+ body = @request . body
251+ body . rewind if body . respond_to? ( :rewind )
252+ body . read
182253 end
183254 end
184255
0 commit comments