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