roda 3.69.0 → 3.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2871ec8db3a18a77238fdae1aeb786c7a7eb93f14b0827428fab1b07f6e7bee8
4
- data.tar.gz: 75db2ec79978055f407c2270cfd9119782322402c2c536a504fbfc08f4fedac7
3
+ metadata.gz: 0d8ea04ca767f8be110f6ba14d1fd877a389a2e733e2925020f2ac589f6a9571
4
+ data.tar.gz: c9a2d26f41724c05ea1de5e8fb0058093e5fad34fb0c6479dd74badcfea67016
5
5
  SHA512:
6
- metadata.gz: 1ca4fdcac9bcba88309b8ccb4315ea2243530b08313135f97a643b81600b98821521fbbe08e9c7ab757732887326e8af492f0f3e883a3f54e7be73f0811d19e6
7
- data.tar.gz: 6b313eecbd4f62d07bca53fa23db2325b4f53f3faf55344c86d8f543ff92c5dcba94235f963582ce227d3602e3ea8633e981393ef6aedcbe20fb8152e6409c08
6
+ metadata.gz: 288a793976f389dfa2fe8fd55fb649590748cbe7ebb9da48f351117bb93da33ceee5848077f53c73430fd943b6be5b69a6d189e8b7424fe9ec60892dbaacff56
7
+ data.tar.gz: 3cad6d6823a2fbcd5534521b6e5fbcc7b85c04d1a80e9ca758cac6ab9b4b967a5f019b653de7d4de3c1d32069832f362fd2564ec219252b0f2dd8ccf1c0785c1
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ = 3.70.0 (2023-07-12)
2
+
3
+ * Add plain_hash_response_headers plugin, using a plain hash for response headers on Rack 3 for much better performance (jeremyevans)
4
+
5
+ * Use lower case response header keys by default on Rack 3, instead of relying on Rack::Headers conversion (jeremyevans)
6
+
1
7
  = 3.69.0 (2023-06-13)
2
8
 
3
9
  * Allow symbol_matcher in symbol_matchers plugin to take a block to allow type conversion (jeremyevans)
@@ -0,0 +1,19 @@
1
+ = New Features
2
+
3
+ * A plain_hash_response_headers plugin has been added. On Rack 3,
4
+ this changes Roda to use a plain hash for response headers (as it
5
+ does on Rack 2), instead of using Rack::Headers (the default on
6
+ Rack 3). For a minimal app, using this plugin can almost double
7
+ the performance on Rack 3. Before using this plugin, you should
8
+ make sure that all response headers set explictly in your
9
+ application are already lower-case.
10
+
11
+ = Improvements
12
+
13
+ * Roda now natively uses lower-case for all response headers set
14
+ implicitly when using Rack 3. Previously, Roda used mixed-case
15
+ response headers and had Rack::Headers handle the conversion to
16
+ lower-case (Rack 3 requires lower-case response headers). Note
17
+ that Rack::Headers is still used for response headers by default
18
+ on Rack 3, as applications may not have converted to using
19
+ lower-case response headers.
@@ -430,8 +430,8 @@ class Roda
430
430
  opts[:css_headers] = headers.merge(opts[:css_headers])
431
431
  opts[:js_headers] = headers.merge(opts[:js_headers])
432
432
  end
433
- opts[:css_headers]['Content-Type'] ||= "text/css; charset=UTF-8".freeze
434
- opts[:js_headers]['Content-Type'] ||= "application/javascript; charset=UTF-8".freeze
433
+ opts[:css_headers][RodaResponseHeaders::CONTENT_TYPE] ||= "text/css; charset=UTF-8".freeze
434
+ opts[:js_headers][RodaResponseHeaders::CONTENT_TYPE] ||= "application/javascript; charset=UTF-8".freeze
435
435
 
436
436
  [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies, :expanded_dependencies].each do |s|
437
437
  opts[s].freeze
@@ -754,7 +754,7 @@ class Roda
754
754
  file = "#{o[:"compiled_#{type}_path"]}#{file}"
755
755
 
756
756
  if o[:gzip] && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/
757
- @_response['Content-Encoding'] = 'gzip'
757
+ @_response[RodaResponseHeaders::CONTENT_ENCODING] = 'gzip'
758
758
  file += '.gz'
759
759
  end
760
760
 
@@ -86,7 +86,7 @@ class Roda
86
86
  return unless time
87
87
  res = response
88
88
  e = env
89
- res['Last-Modified'] = time.httpdate
89
+ res[RodaResponseHeaders::LAST_MODIFIED] = time.httpdate
90
90
  return if e['HTTP_IF_NONE_MATCH']
91
91
  status = res.status
92
92
 
@@ -122,7 +122,7 @@ class Roda
122
122
 
123
123
  res = response
124
124
  e = env
125
- res['ETag'] = etag = "#{'W/' if weak}\"#{value}\""
125
+ res[RodaResponseHeaders::ETAG] = etag = "#{'W/' if weak}\"#{value}\""
126
126
  status = res.status
127
127
 
128
128
  if (!status || (status >= 200 && status < 300) || status == 304)
@@ -176,7 +176,7 @@ class Roda
176
176
  values << (v == true ? k : "#{k}=#{v}")
177
177
  end
178
178
 
179
- self['Cache-Control'] = values.join(', ') unless values.empty?
179
+ @headers[RodaResponseHeaders::CACHE_CONTROL] = values.join(', ') unless values.empty?
180
180
  end
181
181
 
182
182
  # Set Cache-Control header with the max_age given. max_age should
@@ -185,7 +185,7 @@ class Roda
185
185
  # HTTP 1.0 clients (Cache-Control is an HTTP 1.1 header).
186
186
  def expires(max_age, opts=OPTS)
187
187
  cache_control(Hash[opts].merge!(:max_age=>max_age))
188
- self['Expires'] = (Time.now + max_age).httpdate
188
+ @headers[RodaResponseHeaders::EXPIRES] = (Time.now + max_age).httpdate
189
189
  end
190
190
 
191
191
  # Remove Content-Type and Content-Length for 304 responses.
@@ -193,8 +193,8 @@ class Roda
193
193
  a = super
194
194
  if a[0] == 304
195
195
  h = a[1]
196
- h.delete('Content-Type')
197
- h.delete('Content-Length')
196
+ h.delete(RodaResponseHeaders::CONTENT_TYPE)
197
+ h.delete(RodaResponseHeaders::CONTENT_LENGTH)
198
198
  end
199
199
  a
200
200
  end
@@ -257,7 +257,7 @@ class Roda
257
257
  headers.merge!(chunk_headers)
258
258
  end
259
259
  if self.opts[:force_chunked_encoding]
260
- headers['Transfer-Encoding'] = 'chunked'
260
+ res[RodaResponseHeaders::TRANSFER_ENCODING] = 'chunked'
261
261
  body = Body.new(self)
262
262
  else
263
263
  body = StreamBody.new(self)
@@ -46,7 +46,7 @@ class Roda
46
46
 
47
47
  # Log request/response information in common log format to logger.
48
48
  def _roda_after_90__common_logger(result)
49
- return unless result && result[0] && result[1]
49
+ return unless result && (status = result[0]) && (headers = result[1])
50
50
 
51
51
  elapsed_time = if timer = @_request_timer
52
52
  '%0.4f' % (CommonLogger.start_timer - timer)
@@ -56,7 +56,7 @@ class Roda
56
56
 
57
57
  env = @_request.env
58
58
 
59
- line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{result[0]} #{((length = result[1]['Content-Length']) && (length unless length == '0')) || '-'} #{elapsed_time}\n"
59
+ line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{status} #{((length = headers[RodaResponseHeaders::CONTENT_LENGTH]) && (length unless length == '0')) || '-'} #{elapsed_time}\n"
60
60
  if MUTATE_LINE
61
61
  line.gsub!(/[^[:print:]\n]/){|c| sprintf("\\x%x", c.ord)}
62
62
  # :nocov:
@@ -200,7 +200,7 @@ class Roda
200
200
 
201
201
  # The header name to use, depends on whether report only mode has been enabled.
202
202
  def header_key
203
- @report_only ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
203
+ @report_only ? RodaResponseHeaders::CONTENT_SECURITY_POLICY_REPORT_ONLY : RodaResponseHeaders::CONTENT_SECURITY_POLICY
204
204
  end
205
205
 
206
206
  # The header value to use.
@@ -309,7 +309,7 @@ class Roda
309
309
  # Set the appropriate content security policy header.
310
310
  def set_default_headers
311
311
  super
312
- (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(@headers)
312
+ (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(headers)
313
313
  end
314
314
  end
315
315
  end
@@ -27,8 +27,8 @@ class Roda
27
27
  when DROP_BODY_RANGE, 204, 304
28
28
  r[2] = EMPTY_ARRAY
29
29
  h = r[1]
30
- h.delete("Content-Length")
31
- h.delete("Content-Type")
30
+ h.delete(RodaResponseHeaders::CONTENT_LENGTH)
31
+ h.delete(RodaResponseHeaders::CONTENT_TYPE)
32
32
  when 205
33
33
  r[2] = EMPTY_ARRAY
34
34
  empty_205_headers(r[1])
@@ -198,7 +198,7 @@ END
198
198
  def exception_page(exception, opts=OPTS)
199
199
  message = exception_page_exception_message(exception)
200
200
  if opts[:json]
201
- @_response['Content-Type'] = "application/json"
201
+ @_response[RodaResponseHeaders::CONTENT_TYPE] = "application/json"
202
202
  {
203
203
  "exception"=>{
204
204
  "class"=>exception.class.to_s,
@@ -207,7 +207,7 @@ END
207
207
  }
208
208
  }
209
209
  elsif env['HTTP_ACCEPT'] =~ /text\/html/
210
- @_response['Content-Type'] = "text/html"
210
+ @_response[RodaResponseHeaders::CONTENT_TYPE] = "text/html"
211
211
 
212
212
  context = opts[:context] || 7
213
213
  css_file = opts[:css_file]
@@ -394,7 +394,7 @@ END1
394
394
  </html>
395
395
  END
396
396
  else
397
- @_response['Content-Type'] = "text/plain"
397
+ @_response[RodaResponseHeaders::CONTENT_TYPE] = "text/plain"
398
398
  "#{exception.class}: #{message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}"
399
399
  end
400
400
  end
@@ -429,11 +429,11 @@ END
429
429
  # Serve exception page assets
430
430
  def exception_page_assets
431
431
  get 'exception_page.css' do
432
- response['Content-Type'] = "text/css"
432
+ response[RodaResponseHeaders::CONTENT_TYPE] = "text/css"
433
433
  scope.exception_page_css
434
434
  end
435
435
  get 'exception_page.js' do
436
- response['Content-Type'] = "application/javascript"
436
+ response[RodaResponseHeaders::CONTENT_TYPE] = "application/javascript"
437
437
  scope.exception_page_js
438
438
  end
439
439
  end
@@ -45,7 +45,7 @@ class Roda
45
45
  # Match if the given mimetype is one of the accepted mimetypes.
46
46
  def match_accept(mimetype)
47
47
  if @env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
48
- response["Content-Type"] = mimetype
48
+ response[RodaResponseHeaders::CONTENT_TYPE] = mimetype
49
49
  end
50
50
  end
51
51
 
@@ -27,7 +27,7 @@ class Roda
27
27
  if env['PATH_INFO'] == opts[:heartbeat_path]
28
28
  response = @_response
29
29
  response.status = 200
30
- response['Content-Type'] = 'text/plain'
30
+ response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
31
31
  response.write 'OK'
32
32
  throw :halt, response.finish
33
33
  end
@@ -86,7 +86,7 @@ class Roda
86
86
  # Handle a result for one of the registered JSON result classes
87
87
  # by converting the result to JSON.
88
88
  def handle_json_block_result(result)
89
- @_response['Content-Type'] ||= opts[:json_result_content_type]
89
+ @_response[RodaResponseHeaders::CONTENT_TYPE] ||= opts[:json_result_content_type]
90
90
  @_request.send(:convert_to_json, result)
91
91
  end
92
92
  end
@@ -186,7 +186,7 @@ class Roda
186
186
  # that the routing tree did not handle the request.
187
187
  def finish
188
188
  if m = mail
189
- header_content_type = @headers.delete('Content-Type')
189
+ header_content_type = @headers.delete(RodaResponseHeaders::CONTENT_TYPE)
190
190
  m.headers(@headers)
191
191
  m.body(@body.join) unless @body.empty?
192
192
  mail_attachments.each do |a, block|
@@ -241,7 +241,7 @@ class Roda
241
241
  if mail = env['roda.mail']
242
242
  res = @_response
243
243
  res.mail = mail
244
- res.headers.delete('Content-Type')
244
+ res.headers.delete(RodaResponseHeaders::CONTENT_TYPE)
245
245
  end
246
246
  end
247
247
 
@@ -138,7 +138,7 @@ class Roda
138
138
  def method_not_allowed(verbs)
139
139
  res = response
140
140
  res.status = 405
141
- res['Allow'] = verbs
141
+ res[RodaResponseHeaders::ALLOW] = verbs
142
142
  nil
143
143
  end
144
144
  end
@@ -0,0 +1,32 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The response_headers_plain_hash plugin will change Roda to
7
+ # use a plain hash for response headers. This is Roda's
8
+ # default behavior on Rack 2, but on Rack 3+, Roda defaults
9
+ # to using Rack::Headers for response headers for backwards
10
+ # compatibility (Rack::Headers automatically lower cases header
11
+ # keys).
12
+ #
13
+ # On Rack 3+, you should use this plugin for better performance
14
+ # if you are sure all headers in your application and middleware
15
+ # are already lower case (lower case response header keys are
16
+ # required by the Rack 3 spec).
17
+ module PlainHashResponseHeaders
18
+ if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
19
+ module ResponseMethods
20
+ private
21
+
22
+ # Use plain hash for headers
23
+ def _initialize_headers
24
+ {}
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ register_plugin(:plain_hash_response_headers, PlainHashResponseHeaders)
31
+ end
32
+ end
@@ -123,8 +123,8 @@ class Roda
123
123
  headers.replace(h)
124
124
 
125
125
  unless s == 304
126
- headers['Content-Type'] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
127
- headers['Content-Encoding'] = encoding
126
+ headers[RodaResponseHeaders::CONTENT_TYPE] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
127
+ headers[RodaResponseHeaders::CONTENT_ENCODING] = encoding
128
128
  end
129
129
 
130
130
  halt [s, headers, b]
@@ -193,7 +193,10 @@ class Roda
193
193
  raise InvalidToken, msg
194
194
  when :empty_403
195
195
  @_response.status = 403
196
- @_response.headers.replace('Content-Type'=>'text/html', 'Content-Length'=>'0')
196
+ headers = @_response.headers
197
+ headers.clear
198
+ headers[RodaResponseHeaders::CONTENT_TYPE] = 'text/html'
199
+ headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
197
200
  throw :halt, @_response.finish_with_body([])
198
201
  when :clear_session
199
202
  session.clear
@@ -327,7 +327,7 @@ class Roda
327
327
  def send_file(path, opts = OPTS)
328
328
  res = response
329
329
  headers = res.headers
330
- if opts[:type] || !headers["Content-Type"]
330
+ if opts[:type] || !headers[RodaResponseHeaders::CONTENT_TYPE]
331
331
  res.content_type(opts[:type] || ::File.extname(path), :default => 'application/octet-stream')
332
332
  end
333
333
 
@@ -352,7 +352,7 @@ class Roda
352
352
  end
353
353
 
354
354
  res.status = opts[:status] || s
355
- headers.delete("Content-Length")
355
+ headers.delete(RodaResponseHeaders::CONTENT_LENGTH)
356
356
  headers.replace(h.merge!(headers))
357
357
  res.body = b
358
358
 
@@ -407,7 +407,7 @@ class Roda
407
407
 
408
408
  # If the body is a DelayedBody, set the appropriate length for it.
409
409
  def finish
410
- @length = @body.length if @body.is_a?(DelayedBody) && !@headers["Content-Length"]
410
+ @length = @body.length if @body.is_a?(DelayedBody) && !@headers[RodaResponseHeaders::CONTENT_LENGTH]
411
411
  super
412
412
  end
413
413
 
@@ -424,7 +424,7 @@ class Roda
424
424
 
425
425
  # Set the Content-Type of the response body given a media type or file
426
426
  # extension. See plugin documentation for options.
427
- def content_type(type = nil || (return @headers["Content-Type"]), opts = OPTS)
427
+ def content_type(type = nil || (return @headers[RodaResponseHeaders::CONTENT_TYPE]), opts = OPTS)
428
428
  unless (mime_type = mime_type(type) || opts[:default])
429
429
  raise RodaError, "Unknown media type: #{type}"
430
430
  end
@@ -437,7 +437,7 @@ class Roda
437
437
  end
438
438
  end
439
439
 
440
- @headers["Content-Type"] = mime_type
440
+ @headers[RodaResponseHeaders::CONTENT_TYPE] = mime_type
441
441
  end
442
442
 
443
443
  # Set the Content-Disposition to "attachment" with the specified filename,
@@ -463,14 +463,14 @@ class Roda
463
463
  encoded_params = "; filename*=#{encoding.to_s}''#{encoded_filename}"
464
464
  end
465
465
 
466
- unless @headers["Content-Type"]
466
+ unless @headers[RodaResponseHeaders::CONTENT_TYPE]
467
467
  ext = File.extname(filename)
468
468
  unless ext.empty?
469
469
  content_type(ext)
470
470
  end
471
471
  end
472
472
  end
473
- @headers["Content-Disposition"] = "#{disposition}#{params}#{encoded_params}"
473
+ @headers[RodaResponseHeaders::CONTENT_DISPOSITION] = "#{disposition}#{params}#{encoded_params}"
474
474
  end
475
475
 
476
476
  # Whether or not the status is set to 1xx. Returns nil if status not yet set.
@@ -152,7 +152,7 @@ class Roda
152
152
  # the request afterwards, returning the result of the block.
153
153
  def on_type(type, &block)
154
154
  return unless type == requested_type
155
- response['Content-Type'] ||= @scope.opts[:type_routing][:types][type]
155
+ response[RodaResponseHeaders::CONTENT_TYPE] ||= @scope.opts[:type_routing][:types][type]
156
156
  always(&block)
157
157
  end
158
158
 
@@ -200,7 +200,7 @@ class Roda
200
200
  @env['HTTP_ACCEPT'].to_s.split(/\s*,\s*/).map do |part|
201
201
  mime, _= part.split(/\s*;\s*/, 2)
202
202
  if sym = mimes[mime]
203
- response['Vary'] = (vary = response['Vary']) ? "#{vary}, Accept" : 'Accept'
203
+ response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept'
204
204
  return sym
205
205
  end
206
206
  end
data/lib/roda/response.rb CHANGED
@@ -6,6 +6,25 @@ rescue LoadError
6
6
  end
7
7
 
8
8
  class Roda
9
+ # Contains constants for response headers. This approach is used so that all
10
+ # headers used internally by Roda can be lower case on Rack 3, so that it is
11
+ # possible to use a plain hash of response headers instead of using Rack::Headers.
12
+ module RodaResponseHeaders
13
+ headers = %w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length
14
+ Content-Security-Policy Content-Security-Policy-Report-Only Content-Type
15
+ ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary'.freeze.each(&:freeze)
16
+
17
+ if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
18
+ headers.each do |mixed_case|
19
+ const_set(mixed_case.gsub('-', '_').upcase!.to_sym, mixed_case.downcase.freeze)
20
+ end
21
+ else
22
+ headers.each do |mixed_case|
23
+ const_set(mixed_case.gsub('-', '_').upcase!.to_sym, mixed_case.freeze)
24
+ end
25
+ end
26
+ end
27
+
9
28
  # Base class used for Roda responses. The instance methods for this
10
29
  # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class
11
30
  # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
@@ -30,7 +49,7 @@ class Roda
30
49
 
31
50
  # Instance methods for RodaResponse
32
51
  module ResponseMethods
33
- DEFAULT_HEADERS = {"Content-Type" => "text/html".freeze}.freeze
52
+ DEFAULT_HEADERS = {RodaResponseHeaders::CONTENT_TYPE => "text/html".freeze}.freeze
34
53
 
35
54
  # The body for the current response.
36
55
  attr_reader :body
@@ -42,20 +61,11 @@ class Roda
42
61
  # code for non-empty responses and a 404 code for empty responses.
43
62
  attr_accessor :status
44
63
 
45
- if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
46
- # Set the default headers when creating a response.
47
- def initialize
48
- @headers = Rack::Headers.new
49
- @body = []
50
- @length = 0
51
- end
52
- else
53
- # Set the default headers when creating a response.
54
- def initialize
55
- @headers = {}
56
- @body = []
57
- @length = 0
58
- end
64
+ # Set the default headers when creating a response.
65
+ def initialize
66
+ @headers = _initialize_headers
67
+ @body = []
68
+ @length = 0
59
69
  end
60
70
 
61
71
  # Return the response header with the given key. Example:
@@ -108,15 +118,15 @@ class Roda
108
118
  if b.empty?
109
119
  s = @status || 404
110
120
  if (s == 304 || s == 204 || (s >= 100 && s <= 199))
111
- h.delete("Content-Type")
121
+ h.delete(RodaResponseHeaders::CONTENT_TYPE)
112
122
  elsif s == 205
113
123
  empty_205_headers(h)
114
124
  else
115
- h["Content-Length"] ||= '0'
125
+ h[RodaResponseHeaders::CONTENT_LENGTH] ||= '0'
116
126
  end
117
127
  else
118
128
  s = @status || default_status
119
- h["Content-Length"] ||= @length.to_s
129
+ h[RodaResponseHeaders::CONTENT_LENGTH] ||= @length.to_s
120
130
  end
121
131
 
122
132
  [s, h, b]
@@ -149,7 +159,7 @@ class Roda
149
159
  # response.redirect('foo', 301)
150
160
  # response.redirect('bar')
151
161
  def redirect(path, status = 302)
152
- @headers["Location"] = path
162
+ @headers[RodaResponseHeaders::LOCATION] = path
153
163
  @status = status
154
164
  nil
155
165
  end
@@ -171,18 +181,30 @@ class Roda
171
181
 
172
182
  private
173
183
 
184
+ if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
185
+ # Use Rack::Headers for headers by default on Rack 3
186
+ def _initialize_headers
187
+ Rack::Headers.new
188
+ end
189
+ else
190
+ # Use plain hash for headers by default on Rack 1-2
191
+ def _initialize_headers
192
+ {}
193
+ end
194
+ end
195
+
174
196
  if Rack.release < '2.0.2'
175
197
  # Don't use a content length for empty 205 responses on
176
198
  # rack 1, as it violates Rack::Lint in that version.
177
199
  def empty_205_headers(headers)
178
- headers.delete("Content-Type")
179
- headers.delete("Content-Length")
200
+ headers.delete(RodaResponseHeaders::CONTENT_TYPE)
201
+ headers.delete(RodaResponseHeaders::CONTENT_LENGTH)
180
202
  end
181
203
  else
182
204
  # Set the content length for empty 205 responses to 0
183
205
  def empty_205_headers(headers)
184
- headers.delete("Content-Type")
185
- headers["Content-Length"] = '0'
206
+ headers.delete(RodaResponseHeaders::CONTENT_TYPE)
207
+ headers[RodaResponseHeaders::CONTENT_LENGTH] = '0'
186
208
  end
187
209
  end
188
210
 
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 69
7
+ RodaMinorVersion = 70
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
data/lib/roda.rb CHANGED
@@ -204,7 +204,7 @@ class Roda
204
204
 
205
205
  alias set_default_headers set_default_headers
206
206
  def set_default_headers
207
- @headers['Content-Type'] ||= 'text/html'
207
+ @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
208
208
  end
209
209
  end
210
210
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.69.0
4
+ version: 3.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-13 00:00:00.000000000 Z
11
+ date: 2023-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -243,6 +243,7 @@ extra_rdoc_files:
243
243
  - doc/release_notes/3.68.0.txt
244
244
  - doc/release_notes/3.69.0.txt
245
245
  - doc/release_notes/3.7.0.txt
246
+ - doc/release_notes/3.70.0.txt
246
247
  - doc/release_notes/3.8.0.txt
247
248
  - doc/release_notes/3.9.0.txt
248
249
  files:
@@ -319,6 +320,7 @@ files:
319
320
  - doc/release_notes/3.68.0.txt
320
321
  - doc/release_notes/3.69.0.txt
321
322
  - doc/release_notes/3.7.0.txt
323
+ - doc/release_notes/3.70.0.txt
322
324
  - doc/release_notes/3.8.0.txt
323
325
  - doc/release_notes/3.9.0.txt
324
326
  - lib/roda.rb
@@ -412,6 +414,7 @@ files:
412
414
  - lib/roda/plugins/path_matchers.rb
413
415
  - lib/roda/plugins/path_rewriter.rb
414
416
  - lib/roda/plugins/placeholder_string_matchers.rb
417
+ - lib/roda/plugins/plain_hash_response_headers.rb
415
418
  - lib/roda/plugins/precompile_templates.rb
416
419
  - lib/roda/plugins/public.rb
417
420
  - lib/roda/plugins/r.rb