diff --git a/SPEC.rdoc b/SPEC.rdoc index 5d89e09fc..4ebb427ea 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -137,6 +137,14 @@ There are the following restrictions: set. PATH_INFO should be / if SCRIPT_NAME is empty. SCRIPT_NAME never should be /, but instead be empty. +rack.response.finished:: An array of objects responding to #call with one argument, the env hash for the request, that will be called after the HTTP response has been sent to the client. +The callables are called directly after the HTTP response has been sent to the +client. The callables should be called sequentially and synchronously in the +same execution context as the the response. If an exception is raised, it will +be ignored and will not impact the execution of the other callables. The +callbacks will be called event if the request is cancelled (e.g. a user closing +the browser tab before the request completes). Servers supporting this +functionality will prepopulate env with an empty array. === The Input Stream diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb index f084852aa..5a010f637 100644 --- a/lib/rack/constants.rb +++ b/lib/rack/constants.rb @@ -52,6 +52,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' diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index 0f67f2965..889595bbb 100755 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -359,6 +359,23 @@ def check_environment(env) unless env[SCRIPT_NAME] != "/" raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'" end + + ## rack.response.finished:: An array of callables run after the HTTP response has been sent. + 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" unless callable.respond_to?(:call) + + arity = if callable.respond_to?(:arity) + callable.arity + else + callable.method(:call).arity + end + + raise LintError, "rack.response.finished values must accept an env argument" unless arity == 1 + end + end end ## diff --git a/test/spec_lint.rb b/test/spec_lint.rb index 0b87b188a..d5ca8dafc 100755 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -209,6 +209,31 @@ def obj.error(*) 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/) + + lambda { + Rack::Lint.new(nil).call(env("rack.response.finished" => [-> () {}])) + }.must_raise(Rack::Lint::LintError). + message.must_match(/rack.response.finished values must accept an env argument/) + + callable_object = Class.new do + def call + end + end.new + + lambda { + Rack::Lint.new(nil).call(env("rack.response.finished" => [callable_object])) + }.must_raise(Rack::Lint::LintError). + message.must_match(/rack.response.finished values must accept an env argument/) end it "notice input errors" do @@ -708,4 +733,15 @@ def assert_lint(*args) }).call(env({}))[1]['rack.hijack'].call(StringIO.new).read.must_equal '' end + it "pass valid rack.response.finished" do + callable_object = Class.new do + def call(env) + 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