roda 3.69.0 → 3.71.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +10 -0
- data/doc/release_notes/3.70.0.txt +19 -0
- data/doc/release_notes/3.71.0.txt +33 -0
- data/lib/roda/plugins/assets.rb +3 -3
- data/lib/roda/plugins/caching.rb +6 -6
- data/lib/roda/plugins/chunked.rb +1 -1
- data/lib/roda/plugins/common_logger.rb +2 -2
- data/lib/roda/plugins/content_security_policy.rb +2 -2
- data/lib/roda/plugins/drop_body.rb +2 -2
- data/lib/roda/plugins/exception_page.rb +5 -5
- data/lib/roda/plugins/header_matchers.rb +1 -1
- data/lib/roda/plugins/heartbeat.rb +1 -1
- data/lib/roda/plugins/json.rb +4 -1
- data/lib/roda/plugins/mail_processor.rb +1 -1
- data/lib/roda/plugins/mailer.rb +2 -2
- data/lib/roda/plugins/match_hook.rb +3 -1
- data/lib/roda/plugins/match_hook_args.rb +93 -0
- data/lib/roda/plugins/not_allowed.rb +1 -1
- data/lib/roda/plugins/plain_hash_response_headers.rb +32 -0
- data/lib/roda/plugins/public.rb +2 -2
- data/lib/roda/plugins/route_csrf.rb +4 -1
- data/lib/roda/plugins/sinatra_helpers.rb +7 -7
- data/lib/roda/plugins/type_routing.rb +2 -2
- data/lib/roda/plugins/typecast_params.rb +1 -1
- data/lib/roda/response.rb +45 -23
- data/lib/roda/version.rb +1 -1
- data/lib/roda.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5460d6cecb4f9b9acfedd05f5b14793bb9aefa2df2a8c29c559da319df87ee4
|
4
|
+
data.tar.gz: 75c3c8803abef4e27cac592a88597e8f9cd098be39bf1d0c1b1192175fd1f8e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6efc49bb205012a7ce50bc3c300d924a149de33416a35870f11efed492251c331804b970c967e47a481b0ddb0a83507c5926c1dc32069941b4edd2bd56df09e5
|
7
|
+
data.tar.gz: 698b7e7daae4cd0712a20a1e3e274f7ba3cdb9068eefdeb3bb5f043f2d437cdd5dda961ce91d88794e22a8ca3ef5825e3f8bd3f92ff933eab772c352dd17c2ee
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
= 3.71.0 (2023-08-14)
|
2
|
+
|
3
|
+
* Add match_hook_args plugin, similar to match_hooks but support matchers and block args as hook arguments (jeremyevans)
|
4
|
+
|
5
|
+
= 3.70.0 (2023-07-12)
|
6
|
+
|
7
|
+
* Add plain_hash_response_headers plugin, using a plain hash for response headers on Rack 3 for much better performance (jeremyevans)
|
8
|
+
|
9
|
+
* Use lower case response header keys by default on Rack 3, instead of relying on Rack::Headers conversion (jeremyevans)
|
10
|
+
|
1
11
|
= 3.69.0 (2023-06-13)
|
2
12
|
|
3
13
|
* 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.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
= New Feature
|
2
|
+
|
3
|
+
* A match_hook_args plugin has been added. This is similar to the
|
4
|
+
existing match_hook plugin, but passes through the matchers and
|
5
|
+
block arguments (values yielded to the match block). Example:
|
6
|
+
|
7
|
+
plugin :match_hook_args
|
8
|
+
|
9
|
+
add_match_hook do |matchers, block_args|
|
10
|
+
logger.debug("matchers: #{matchers.inspect}. #{block_args.inspect} yielded.")
|
11
|
+
end
|
12
|
+
|
13
|
+
# Term is an implicit matcher used for terminating matches, and
|
14
|
+
# will be included in the array of matchers yielded to the match hook
|
15
|
+
# if a terminating match is used.
|
16
|
+
term = self.class::RodaRequest::TERM
|
17
|
+
|
18
|
+
route do |r|
|
19
|
+
r.root do
|
20
|
+
# for a request for /
|
21
|
+
# matchers: nil, block_args: nil
|
22
|
+
end
|
23
|
+
|
24
|
+
r.on 'a', ['b', 'c'], Integer do |segment, id|
|
25
|
+
# for a request for /a/b/1
|
26
|
+
# matchers: ["a", ["b", "c"], Integer], block_args: ["b", 1]
|
27
|
+
end
|
28
|
+
|
29
|
+
r.get 'd' do
|
30
|
+
# for a request for /d
|
31
|
+
# matchers: ["d", term], block_args: []
|
32
|
+
end
|
33
|
+
end
|
data/lib/roda/plugins/assets.rb
CHANGED
@@ -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][
|
434
|
-
opts[:js_headers][
|
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[
|
757
|
+
@_response[RodaResponseHeaders::CONTENT_ENCODING] = 'gzip'
|
758
758
|
file += '.gz'
|
759
759
|
end
|
760
760
|
|
data/lib/roda/plugins/caching.rb
CHANGED
@@ -86,7 +86,7 @@ class Roda
|
|
86
86
|
return unless time
|
87
87
|
res = response
|
88
88
|
e = env
|
89
|
-
res[
|
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[
|
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
|
-
|
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
|
-
|
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(
|
197
|
-
h.delete(
|
196
|
+
h.delete(RodaResponseHeaders::CONTENT_TYPE)
|
197
|
+
h.delete(RodaResponseHeaders::CONTENT_LENGTH)
|
198
198
|
end
|
199
199
|
a
|
200
200
|
end
|
data/lib/roda/plugins/chunked.rb
CHANGED
@@ -257,7 +257,7 @@ class Roda
|
|
257
257
|
headers.merge!(chunk_headers)
|
258
258
|
end
|
259
259
|
if self.opts[:force_chunked_encoding]
|
260
|
-
|
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}\" #{
|
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 ?
|
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(
|
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(
|
31
|
-
h.delete(
|
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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
30
|
+
response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
|
31
31
|
response.write 'OK'
|
32
32
|
throw :halt, response.finish
|
33
33
|
end
|
data/lib/roda/plugins/json.rb
CHANGED
@@ -52,6 +52,9 @@ class Roda
|
|
52
52
|
# using the +:content_type+ option:
|
53
53
|
#
|
54
54
|
# plugin :json, content_type: 'application/xml'
|
55
|
+
#
|
56
|
+
# This plugin depends on the custom_block_results plugin, and therefore does
|
57
|
+
# not support treating String, FalseClass, or NilClass values as JSON.
|
55
58
|
module Json
|
56
59
|
# Set the classes to automatically convert to JSON, and the serializer to use.
|
57
60
|
def self.configure(app, opts=OPTS)
|
@@ -86,7 +89,7 @@ class Roda
|
|
86
89
|
# Handle a result for one of the registered JSON result classes
|
87
90
|
# by converting the result to JSON.
|
88
91
|
def handle_json_block_result(result)
|
89
|
-
@_response[
|
92
|
+
@_response[RodaResponseHeaders::CONTENT_TYPE] ||= opts[:json_result_content_type]
|
90
93
|
@_request.send(:convert_to_json, result)
|
91
94
|
end
|
92
95
|
end
|
@@ -379,7 +379,7 @@ class Roda
|
|
379
379
|
end
|
380
380
|
|
381
381
|
# Perform the processing of mail for this request, first considering
|
382
|
-
# routes defined via the
|
382
|
+
# routes defined via the class-level +rcpt+ method, and then the
|
383
383
|
# normal routing tree passed in as the block.
|
384
384
|
def process_mail(&block)
|
385
385
|
if string_routes = opts[:mail_processor_string_routes]
|
data/lib/roda/plugins/mailer.rb
CHANGED
@@ -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(
|
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(
|
244
|
+
res.headers.delete(RodaResponseHeaders::CONTENT_TYPE)
|
245
245
|
end
|
246
246
|
end
|
247
247
|
|
@@ -4,7 +4,9 @@
|
|
4
4
|
class Roda
|
5
5
|
module RodaPlugins
|
6
6
|
# The match_hook plugin adds hooks that are called upon a successful match
|
7
|
-
# by any of the matchers.
|
7
|
+
# by any of the matchers. The hooks do not take any arguments. If you would
|
8
|
+
# like hooks that pass the arguments/matchers and values yielded to the route block,
|
9
|
+
# use the match_hook_args plugin.
|
8
10
|
#
|
9
11
|
# plugin :match_hook
|
10
12
|
#
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The match_hook_args plugin adds hooks that are called upon a successful match
|
7
|
+
# by any of the matchers. It is similar to the match_hook plugin, but it allows
|
8
|
+
# for passing the matchers and block arguments for each match method.
|
9
|
+
#
|
10
|
+
# plugin :match_hook_args
|
11
|
+
#
|
12
|
+
# add_match_hook do |matchers, block_args|
|
13
|
+
# logger.debug("matchers: #{matchers.inspect}. #{block_args.inspect} yielded.")
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # Term is an implicit matcher used for terminating matches, and
|
17
|
+
# # will be included in the array of matchers yielded to the match hook
|
18
|
+
# # if a terminating match is used.
|
19
|
+
# term = self.class::RodaRequest::TERM
|
20
|
+
#
|
21
|
+
# route do |r|
|
22
|
+
# r.root do
|
23
|
+
# # for a request for /
|
24
|
+
# # matchers: nil, block_args: nil
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# r.on 'a', ['b', 'c'], Integer do |segment, id|
|
28
|
+
# # for a request for /a/b/1
|
29
|
+
# # matchers: ["a", ["b", "c"], Integer], block_args: ["b", 1]
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# r.get 'd' do
|
33
|
+
# # for a request for /d
|
34
|
+
# # matchers: ["d", term], block_args: []
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
module MatchHookArgs
|
38
|
+
def self.configure(app)
|
39
|
+
app.opts[:match_hook_args] ||= []
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
# Freeze the array of hook methods when freezing the app
|
44
|
+
def freeze
|
45
|
+
opts[:match_hook_args].freeze
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
# Add a match hook that will be called with matchers and block args.
|
50
|
+
def add_match_hook(&block)
|
51
|
+
opts[:match_hook_args] << define_roda_method("match_hook_args", :any, &block)
|
52
|
+
|
53
|
+
if opts[:match_hook_args].length == 1
|
54
|
+
class_eval("alias _match_hook_args #{opts[:match_hook_args].first}", __FILE__, __LINE__)
|
55
|
+
else
|
56
|
+
class_eval("def _match_hook_args(v, a); #{opts[:match_hook_args].map{|m| "#{m}(v, a)"}.join(';')} end", __FILE__, __LINE__)
|
57
|
+
end
|
58
|
+
|
59
|
+
public :_match_hook_args
|
60
|
+
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module InstanceMethods
|
66
|
+
# Default empty method if no match hooks are defined.
|
67
|
+
def _match_hook_args(matchers, block_args)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module RequestMethods
|
72
|
+
private
|
73
|
+
|
74
|
+
# Call the match hook with matchers and block args if yielding to the block before yielding to the block.
|
75
|
+
def if_match(v)
|
76
|
+
super do |*a|
|
77
|
+
scope._match_hook_args(v, a)
|
78
|
+
yield(*a)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Call the match hook with nil matchers and blocks before yielding to the block
|
83
|
+
def always
|
84
|
+
scope._match_hook_args(nil, nil)
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
register_plugin :match_hook_args, MatchHookArgs
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
@@ -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
|
data/lib/roda/plugins/public.rb
CHANGED
@@ -123,8 +123,8 @@ class Roda
|
|
123
123
|
headers.replace(h)
|
124
124
|
|
125
125
|
unless s == 304
|
126
|
-
headers[
|
127
|
-
headers[
|
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
|
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[
|
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(
|
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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
203
|
+
response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept'
|
204
204
|
return sym
|
205
205
|
end
|
206
206
|
end
|
@@ -346,7 +346,7 @@ class Roda
|
|
346
346
|
end
|
347
347
|
|
348
348
|
# The reason behind this error. If this error was caused by a conversion method,
|
349
|
-
# this will be the
|
349
|
+
# this will be the conversion method symbol. If this error was caused
|
350
350
|
# because a value was missing, then it will be +:missing+. If this error was
|
351
351
|
# caused because a value was not the correct type, then it will be +:invalid_type+.
|
352
352
|
attr_accessor :reason
|
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 = {
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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(
|
121
|
+
h.delete(RodaResponseHeaders::CONTENT_TYPE)
|
112
122
|
elsif s == 205
|
113
123
|
empty_205_headers(h)
|
114
124
|
else
|
115
|
-
h[
|
125
|
+
h[RodaResponseHeaders::CONTENT_LENGTH] ||= '0'
|
116
126
|
end
|
117
127
|
else
|
118
128
|
s = @status || default_status
|
119
|
-
h[
|
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[
|
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(
|
179
|
-
headers.delete(
|
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(
|
185
|
-
headers[
|
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
data/lib/roda.rb
CHANGED
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.
|
4
|
+
version: 3.71.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-
|
11
|
+
date: 2023-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -243,6 +243,8 @@ 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
|
247
|
+
- doc/release_notes/3.71.0.txt
|
246
248
|
- doc/release_notes/3.8.0.txt
|
247
249
|
- doc/release_notes/3.9.0.txt
|
248
250
|
files:
|
@@ -319,6 +321,8 @@ files:
|
|
319
321
|
- doc/release_notes/3.68.0.txt
|
320
322
|
- doc/release_notes/3.69.0.txt
|
321
323
|
- doc/release_notes/3.7.0.txt
|
324
|
+
- doc/release_notes/3.70.0.txt
|
325
|
+
- doc/release_notes/3.71.0.txt
|
322
326
|
- doc/release_notes/3.8.0.txt
|
323
327
|
- doc/release_notes/3.9.0.txt
|
324
328
|
- lib/roda.rb
|
@@ -389,6 +393,7 @@ files:
|
|
389
393
|
- lib/roda/plugins/mailer.rb
|
390
394
|
- lib/roda/plugins/match_affix.rb
|
391
395
|
- lib/roda/plugins/match_hook.rb
|
396
|
+
- lib/roda/plugins/match_hook_args.rb
|
392
397
|
- lib/roda/plugins/middleware.rb
|
393
398
|
- lib/roda/plugins/middleware_stack.rb
|
394
399
|
- lib/roda/plugins/module_include.rb
|
@@ -412,6 +417,7 @@ files:
|
|
412
417
|
- lib/roda/plugins/path_matchers.rb
|
413
418
|
- lib/roda/plugins/path_rewriter.rb
|
414
419
|
- lib/roda/plugins/placeholder_string_matchers.rb
|
420
|
+
- lib/roda/plugins/plain_hash_response_headers.rb
|
415
421
|
- lib/roda/plugins/precompile_templates.rb
|
416
422
|
- lib/roda/plugins/public.rb
|
417
423
|
- lib/roda/plugins/r.rb
|