Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify rack.provisional_response to send 1xx responses #1701

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions SPEC.rdoc
Expand Up @@ -138,6 +138,7 @@ There are the following restrictions:
* There must be a valid input stream in <tt>rack.input</tt>.
* There must be a valid error stream in <tt>rack.errors</tt>.
* There may be a valid hijack stream in <tt>rack.hijack_io</tt>
* There may be a valid provisional response callback in <tt>rack.provisional_response</tt>
* The <tt>REQUEST_METHOD</tt> must be a valid token.
* The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
* The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
Expand Down Expand Up @@ -244,6 +245,22 @@ if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
* Middleware may wrap the IO object for the response pattern.
* Middleware should not wrap the IO object for the request pattern. The
request pattern is intended to provide the hijacker with "raw tcp".
=== Provisional Response

The application or any middleware may call the <tt>rack.provisional_response</tt>
with a response code and headers that will be sent as a 1xx provisional response.

If rack.early_hints is present it must respond to #call.
This is an HTTP status. It must be an Integer greater than or equal to
100 and lower than 200.
The header must respond to +each+, and yield values of key and value.
The header keys must be Strings.
The header must conform to RFC7230 token specification, i.e. cannot
contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
The values of the header must be Strings,
consisting of lines (for multiple header values, e.g. multiple
<tt>Set-Cookie</tt> values) separated by "\\n".
The lines must not contain characters below 037.
== The Response
=== The Status
This is an HTTP status. It must be an Integer greater than or equal to
Expand Down
1 change: 1 addition & 0 deletions lib/rack.rb
Expand Up @@ -62,6 +62,7 @@ module Rack
RACK_HIJACK = 'rack.hijack'
RACK_IS_HIJACK = 'rack.hijack?'
RACK_HIJACK_IO = 'rack.hijack_io'
RACK_PROVISIONAL_RESPONSE = 'rack.provisional_response'
RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'
RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'
RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'
Expand Down
51 changes: 51 additions & 0 deletions lib/rack/lint.rb
Expand Up @@ -335,6 +335,8 @@ def check_env(env)
check_error env[RACK_ERRORS]
## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
check_hijack env
## * There may be a valid provisional response callback in <tt>rack.provisional_response</tt>
check_provisional_response env

## * The <tt>REQUEST_METHOD</tt> must be a valid token.
assert("REQUEST_METHOD unknown: #{env[REQUEST_METHOD]}") {
Expand Down Expand Up @@ -655,6 +657,55 @@ def check_hijack_response(headers, env)
## * Middleware should not wrap the IO object for the request pattern. The
## request pattern is intended to provide the hijacker with "raw tcp".

## === Provisional Response
##
## The application or any middleware may call the <tt>rack.provisional_response</tt>
## with a response code and headers that will be sent as a 1xx provisional response.
def check_provisional_response(env)
if env[RACK_PROVISIONAL_RESPONSE]
##
## If rack.early_hints is present it must respond to #call.
assert("rack.early_hints must respond to call") { env[RACK_PROVISIONAL_RESPONSE].respond_to?(:call) }
original_callback = env[RACK_PROVISIONAL_RESPONSE]
env[RACK_PROVISIONAL_RESPONSE] = lambda do |status, headers|
## This is an HTTP status. It must be an Integer greater than or equal to
## 100 and lower than 200.
assert("Status must be an Integer >=100 and < 200") {
status.is_a?(Integer) && status >= 100 && status < 200
}

## The header must respond to +each+, and yield values of key and value.
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
header.respond_to? :each
}

header.each { |key, value|
## The header keys must be Strings.
assert("header key must be a string, was #{key.class}") {
key.kind_of? String
}

## The header must conform to RFC7230 token specification, i.e. cannot
## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
assert("invalid header name: #{key}") { key !~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ }

## The values of the header must be Strings,
assert("a header value must be a String, but the value of " +
"'#{key}' is a #{value.class}") { value.kind_of? String }
## consisting of lines (for multiple header values, e.g. multiple
## <tt>Set-Cookie</tt> values) separated by "\\n".
value.split("\n").each { |item|
## The lines must not contain characters below 037.
assert("invalid header value #{key}: #{item.inspect}") {
item !~ /[\000-\037]/
}
}
}
original_callback.call(status, headers)
end
end
end

## == The Response

## === The Status
Expand Down