From 0cc589a1bf8248c3d209fb6265ff712b07793c76 Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Fri, 4 Feb 2022 10:56:05 -0500 Subject: [PATCH 1/9] 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` --- SPEC.rdoc | 1 + lib/rack/constants.rb | 1 + lib/rack/lint.rb | 9 +++++++++ test/spec_lint.rb | 16 ++++++++++++++++ 4 files changed, 27 insertions(+) diff --git a/SPEC.rdoc b/SPEC.rdoc index 5d89e09fc..72a5d41f2 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -137,6 +137,7 @@ 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 callables run after the HTTP response has been sent. === The Input Stream diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb index f084852aa..63fd0565a 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..ea99a215b 100755 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -359,6 +359,15 @@ 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) + end + end end ## diff --git a/test/spec_lint.rb b/test/spec_lint.rb index 0b87b188a..d9f2c2b2d 100755 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -209,6 +209,16 @@ 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" => [->{}, "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 @@ -708,4 +718,10 @@ def assert_lint(*args) }).call(env({}))[1]['rack.hijack'].call(StringIO.new).read.must_equal '' end + it "pass valid rack.response_finished" do + Rack::Lint.new(lambda { |env| + [200, { "rack.response_finished" => [-> {}, lambda {}], "Content-length" => "3" }, ["foo"]] + }).call(env({})).first.must_equal 200 + end + end From 0a2c349cc7c54c575a268d9be4fbcb7140e00501 Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Tue, 5 Apr 2022 17:24:20 -0400 Subject: [PATCH 2/9] Fix test error due to casing --- test/spec_lint.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec_lint.rb b/test/spec_lint.rb index d9f2c2b2d..f34ede728 100755 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -720,7 +720,7 @@ def assert_lint(*args) it "pass valid rack.response_finished" do Rack::Lint.new(lambda { |env| - [200, { "rack.response_finished" => [-> {}, lambda {}], "Content-length" => "3" }, ["foo"]] + [200, { "rack.response_finished" => [-> {}, lambda {}], "content-length" => "3" }, ["foo"]] }).call(env({})).first.must_equal 200 end From 20da7c3f9261eac257bd1985ae6327e6a9d158e8 Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Fri, 8 Apr 2022 10:12:57 -0400 Subject: [PATCH 3/9] Add more details to spec --- SPEC.rdoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SPEC.rdoc b/SPEC.rdoc index 72a5d41f2..e230a1921 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -138,6 +138,9 @@ There are the following restrictions: SCRIPT_NAME is empty. SCRIPT_NAME never should be /, but instead be empty. rack.response_finished:: An array of callables run after the HTTP response has been sent. +The array of callables should be called directly after the HTTP response has +been closed. The callables should be called sequentially and synchronously. + === The Input Stream From 32d4b96b1e64ada10363d94b5ea912f3cf2e8a2e Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Mon, 11 Apr 2022 13:13:20 -0400 Subject: [PATCH 4/9] Update SPEC.rdoc Co-authored-by: Samuel Williams --- SPEC.rdoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SPEC.rdoc b/SPEC.rdoc index e230a1921..f677d6f3b 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -139,7 +139,9 @@ There are the following restrictions: SCRIPT_NAME never should be /, but instead be empty. rack.response_finished:: An array of callables run after the HTTP response has been sent. The array of callables should be called directly after the HTTP response has -been closed. The callables should be called sequentially and synchronously. +been completely sent. The callables should be called sequentially and synchronously +in the same execution context as the response. If an exception is raised, it will +be ignored and will not impact the execution of other callables. === The Input Stream From db15309806ac395bbeb0670dd469e50c4f3fe3c5 Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Tue, 28 Jun 2022 09:45:03 -0400 Subject: [PATCH 5/9] Pass env, support objects that respond to #call --- lib/rack/lint.rb | 8 ++++++++ test/spec_lint.rb | 26 +++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index ea99a215b..125c84b5b 100755 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -366,6 +366,14 @@ def check_environment(env) 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 f34ede728..5cf017652 100755 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -216,9 +216,24 @@ def obj.error(*) end message.must_match(/rack.response_finished must be an array of callable objects/) lambda { - Rack::Lint.new(nil).call(env("rack.response_finished" => [->{}, "not a callable"])) + 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 @@ -719,9 +734,14 @@ def assert_lint(*args) 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, { "rack.response_finished" => [-> {}, lambda {}], "content-length" => "3" }, ["foo"]] - }).call(env({})).first.must_equal 200 + [200, {}, ["foo"]] + }).call(env({ "rack.response_finished" => [-> (env) {}, lambda { |env| }, callable_object], "content-length" => "3" })).first.must_equal 200 end end From 8f343bde71e25a792780b2ad8d7d488ee82c6c76 Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Tue, 28 Jun 2022 09:45:18 -0400 Subject: [PATCH 6/9] Update spec description --- SPEC.rdoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SPEC.rdoc b/SPEC.rdoc index f677d6f3b..227b9772a 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -137,11 +137,11 @@ 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 callables run after the HTTP response has been sent. -The array of callables should be called directly after the HTTP response has -been completely sent. The callables should be called sequentially and synchronously -in the same execution context as the response. If an exception is raised, it will -be ignored and will not impact the execution of other callables. +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 Input Stream From d54a3cf52c0808a8661773b31209823c7b7b188f Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Mon, 11 Jul 2022 11:40:25 -0400 Subject: [PATCH 7/9] rack.response_finished -> rack.response.finished --- SPEC.rdoc | 7 +------ lib/rack/constants.rb | 2 +- lib/rack/lint.rb | 8 ++++---- test/spec_lint.rb | 20 ++++++++++---------- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/SPEC.rdoc b/SPEC.rdoc index 227b9772a..0b13df3e6 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -137,12 +137,7 @@ 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. - +rack.response.finished:: An array of callables run after the HTTP response has been sent. === The Input Stream diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb index 63fd0565a..5a010f637 100644 --- a/lib/rack/constants.rb +++ b/lib/rack/constants.rb @@ -52,7 +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_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 125c84b5b..889595bbb 100755 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -360,12 +360,12 @@ def check_environment(env) 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. + ## 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) + 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) + raise LintError, "rack.response.finished values must respond to call" unless callable.respond_to?(:call) arity = if callable.respond_to?(:arity) callable.arity @@ -373,7 +373,7 @@ def check_environment(env) callable.method(:call).arity end - raise LintError, "rack.response_finished values must accept an env argument" unless arity == 1 + 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 5cf017652..d5ca8dafc 100755 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -211,19 +211,19 @@ def obj.error(*) end message.must_match(/cannot be .* make it ''/) lambda { - Rack::Lint.new(nil).call(env("rack.response_finished" => "not a callable")) + 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/) + 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"])) + 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/) + message.must_match(/rack.response.finished values must respond to call/) lambda { - Rack::Lint.new(nil).call(env("rack.response_finished" => [-> () {}])) + 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/) + message.must_match(/rack.response.finished values must accept an env argument/) callable_object = Class.new do def call @@ -231,9 +231,9 @@ def call end.new lambda { - Rack::Lint.new(nil).call(env("rack.response_finished" => [callable_object])) + 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/) + message.must_match(/rack.response.finished values must accept an env argument/) end it "notice input errors" do @@ -733,7 +733,7 @@ def assert_lint(*args) }).call(env({}))[1]['rack.hijack'].call(StringIO.new).read.must_equal '' end - it "pass valid rack.response_finished" do + it "pass valid rack.response.finished" do callable_object = Class.new do def call(env) end @@ -741,7 +741,7 @@ def call(env) Rack::Lint.new(lambda { |env| [200, {}, ["foo"]] - }).call(env({ "rack.response_finished" => [-> (env) {}, lambda { |env| }, callable_object], "content-length" => "3" })).first.must_equal 200 + }).call(env({ "rack.response.finished" => [-> (env) {}, lambda { |env| }, callable_object], "content-length" => "3" })).first.must_equal 200 end end From 57aaaac2e321c2d23bc1cac2c54a9eaf6a8366bb Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Mon, 11 Jul 2022 11:41:16 -0400 Subject: [PATCH 8/9] Spec docs --- SPEC.rdoc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SPEC.rdoc b/SPEC.rdoc index 0b13df3e6..5f1c416fb 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -137,7 +137,15 @@ 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 callables run after the HTTP response has been sent. +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 From 56b3ae845e98b0afa894bdf48ea21561bb565e12 Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Mon, 11 Jul 2022 11:41:56 -0400 Subject: [PATCH 9/9] Remove extra newline --- SPEC.rdoc | 1 - 1 file changed, 1 deletion(-) diff --git a/SPEC.rdoc b/SPEC.rdoc index 5f1c416fb..4ebb427ea 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -146,7 +146,6 @@ 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 The input stream is an IO-like object which contains the raw HTTP