Skip to content

Commit

Permalink
Add rack.response_finished to Rack::Lint. (#1952)
Browse files Browse the repository at this point in the history
* Add rack.response_finished to Rack::Lint

This updates Rack::Lint to validate that `rack.response_finished` is an
array of callables when present in the `env`. e.g. procs, lambdas, or
objects that respond to `call`.

This validates that:

* `rack.response_finished` is an array
* The contents of the array all respond to `call`
  • Loading branch information
ioquatix committed Aug 26, 2022
1 parent 1a37044 commit 856c4f9
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 2 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Expand Up @@ -19,9 +19,10 @@ All notable changes to this project will be documented in this file. For info on
- Middleware must no longer call `#each` on the body, but they can call `#to_ary` on the body if it responds to `#to_ary`.
- `rack.input` is no longer required to be rewindable.
- `rack.multithread`/`rack.multiprocess`/`rack.run_once`/`rack.version` are no longer required environment keys.
- `SERVER_PROTOCOL` is now a required key, matching the HTTP protocol used in the request.
- `SERVER_PROTOCOL` is now a required environment key, matching the HTTP protocol used in the request.
- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional.
- `rack.hijack_io` has been removed completely.
- `rack.response_finished` is an optional environment key which contains an array of callable objects that must accept `#call(env, status, headers, error)` and are invoked after the response is finished (either successfully or unsucessfully).

### Removed

Expand All @@ -41,6 +42,7 @@ All notable changes to this project will be documented in this file. For info on
- Allow response headers to contain array of values. ([#1598](https://github.com/rack/rack/issues/1598), [@ioquatix])
- Support callable body for explicit streaming support and clarify streaming response body behaviour. ([#1745](https://github.com/rack/rack/pull/1745), [@ioquatix], [#1748](https://github.com/rack/rack/pull/1748), [@wjordan])
- Allow `Rack::Builder#run` to take a block instead of an argument. ([#1942](https://github.com/rack/rack/pull/1942), [@ioquatix])
- Add `rack.response_finished` to `Rack::Lint`. ([#1802](https://github.com/rack/rack/pull/1802), [@BlakeWilliams], [#1952](https://github.com/rack/rack/pull/1952), [@ioquatix])

### Changed

Expand Down Expand Up @@ -787,3 +789,4 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md
[@jeremyevans]: https://github.com/jeremyevans "Jeremy Evans"
[@amatsuda]: https://github.com/amatsuda "Akira Matsuda"
[@wjordan]: https://github.com/wjordan "Will Jordan"
[@BlakeWilliams]: https://github.com/BlakeWilliams "Blake Williams"
6 changes: 6 additions & 0 deletions SPEC.rdoc
Expand Up @@ -132,6 +132,12 @@ There are the following restrictions:
set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
<tt>SCRIPT_NAME</tt> is empty.
<tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
<tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been
processed. This would typically be invoked after sending the response to the client, but it could also be
invoked if an error occurs while generating the response or sending the response; in that case, the error
argument will be a subclass of +Exception+.
The callables are invoked with +env, status, headers, error+ arguments and should not raise any
exceptions. They should be invoked in reverse order of registration.

=== The Input Stream

Expand Down
1 change: 1 addition & 0 deletions lib/rack/constants.rb
Expand Up @@ -51,6 +51,7 @@ module Rack
RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'
RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'
RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'
RACK_RESPONSE_FINISHED = 'rack.response_finished'
RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'
Expand Down
16 changes: 15 additions & 1 deletion lib/rack/lint.rb
Expand Up @@ -363,6 +363,20 @@ def check_environment(env)
unless env[SCRIPT_NAME] != "/"
raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'"
end

## <tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been
## processed. This would typically be invoked after sending the response to the client, but it could also be
## invoked if an error occurs while generating the response or sending the response; in that case, the error
## argument will be a subclass of +Exception+.
## The callables are invoked with +env, status, headers, error+ arguments and should not raise any
## exceptions. They should be invoked in reverse order of registration.
if callables = env[RACK_RESPONSE_FINISHED]
raise LintError, "rack.response_finished must be an array of callable objects" unless callables.is_a?(Array)

callables.each do |callable|
raise LintError, "rack.response_finished values must respond to call(env, status, headers, error)" unless callable.respond_to?(:call)
end
end
end

##
Expand Down Expand Up @@ -582,7 +596,7 @@ def check_hijack_response(headers, env)
## ignore the +body+ part of the response tuple when the
## +rack.hijack+ response header is present. Using an empty +Array+
## instance is recommended.
else
else
##
## The special response header +rack.hijack+ must only be set
## if the request +env+ has a truthy +rack.hijack?+.
Expand Down
21 changes: 21 additions & 0 deletions test/spec_lint.rb
Expand Up @@ -253,6 +253,16 @@ def obj.fatal(*) end
Rack::Lint.new(nil).call(env("SCRIPT_NAME" => "/"))
}.must_raise(Rack::Lint::LintError).
message.must_match(/cannot be .* make it ''/)

lambda {
Rack::Lint.new(nil).call(env("rack.response_finished" => "not a callable"))
}.must_raise(Rack::Lint::LintError).
message.must_match(/rack.response_finished must be an array of callable objects/)

lambda {
Rack::Lint.new(nil).call(env("rack.response_finished" => [-> (env) {}, "not a callable"]))
}.must_raise(Rack::Lint::LintError).
message.must_match(/rack.response_finished values must respond to call/)
end

it "notice input errors" do
Expand Down Expand Up @@ -811,4 +821,15 @@ def assert_lint(*args)
}.must_raise(Rack::Lint::LintError).
message.must_equal 'rack.hijack header must respond to #call'
end

it "pass valid rack.response_finished" do
callable_object = Class.new do
def call(env, status, headers, error)
end
end.new

Rack::Lint.new(lambda { |env|
[200, {}, ["foo"]]
}).call(env({ "rack.response_finished" => [-> (env) {}, lambda { |env| }, callable_object], "content-length" => "3" })).first.must_equal 200
end
end

0 comments on commit 856c4f9

Please sign in to comment.