class Generic
The base adapter class that provides common functionality for all Rack adapters.
It handles the conversion between Protocol::HTTP and Rack environments.
Definitions
def self.wrap(app)
Creates a new adapter instance for the given Rack application.
Wraps the adapter in a class Protocol::Rack::Rewindable instance to ensure request body can be read multiple times, which is required for Rack < 3.
Signature
-
parameter
appInterface(:call) A Rack application.
-
returns
Rewindable A rewindable adapter instance.
Implementation
def self.wrap(app)
Rewindable.new(self.new(app))
end
def self.parse_file(...)
Parses a Rackup file and returns the application.
Signature
-
parameter
pathString The path to the Rackup file.
-
returns
Interface(:call) The Rack application.
Implementation
def self.parse_file(...)
# This is the old interface, which was changed in Rack 3:
::Rack::Builder.parse_file(...).first
end
def initialize(app)
Initialize the Rack adapter middleware.
Signature
-
parameter
appInterface(:call) The Rack middleware.
-
raises
ArgumentError If the app does not respond to
call.
Implementation
def initialize(app)
@app = app
raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call)
end
def logger
The logger to use for this adapter.
Signature
-
returns
Console The console logger.
Implementation
def logger
Console
end
def unwrap_headers(headers, env)
Unwrap HTTP headers into the CGI-style expected by Rack middleware, and add them to the Rack env.
For example, accept-encoding becomes HTTP_ACCEPT_ENCODING.
Header keys with underscores will generate the same CGI-style header key as headers with dashes.
For example, accept_encoding becomes HTTP_ACCEPT_ENCODING too.
You should not implicitly trust the HTTP_ headers for security purposes, as they are generated by the client.
Multiple headers are combined with a comma, with one exception: HTTP_COOKIE headers are combined with a semicolon.
Signature
-
parameter
headersProtocol::HTTP::Headers The raw HTTP request headers.
-
parameter
envHash The Rack request
env.
Implementation
def unwrap_headers(headers, env)
headers.each do |key, value|
http_key = "HTTP_#{key.upcase.tr('-', '_')}"
if current_value = env[http_key]
if http_key == CGI::HTTP_COOKIE
env[http_key] = "#{current_value};#{value}"
else
env[http_key] = "#{current_value},#{value}"
end
else
env[http_key] = value.to_s
end
end
end
def unwrap_request(request, env)
Process the incoming request into a valid Rack env.
- Set the
env['CONTENT_TYPE']andenv['CONTENT_LENGTH']based on the incoming request body. - Set the
env['HTTP_HOST']header to the request authority. - Set the
env['HTTP_X_FORWARDED_PROTO']header to the request scheme. - Set
env['REMOTE_ADDR']to the request remote address.
Signature
-
parameter
requestProtocol::HTTP::Request The incoming request.
-
parameter
envHash The rack
env.
Implementation
def unwrap_request(request, env)
# The request protocol, either from the upgrade header or the HTTP/2 pseudo header of the same name:
if protocol = request.protocol
env[RACK_PROTOCOL] = protocol
end
if content_type = request.headers.delete("content-type")
env[CGI::CONTENT_TYPE] = content_type
end
# In some situations we don't know the content length, e.g. when using chunked encoding, or when decompressing the body:
if body = request.body and length = body.length
env[CGI::CONTENT_LENGTH] = length.to_s
end
# We ignore trailers for the purpose of constructing the rack environment:
self.unwrap_headers(request.headers.header, env)
# For the sake of compatibility, we set the `HTTP_UPGRADE` header to the requested protocol:
if protocol = request.protocol and request.version.start_with?("HTTP/1")
env[CGI::HTTP_UPGRADE] = Array(protocol).join(",")
end
if request.respond_to?(:hijack?) and request.hijack?
env[RACK_IS_HIJACK] = true
env[RACK_HIJACK] = proc{request.hijack!.io}
end
# HTTP/2 prefers `:authority` over `host`, so we do this for backwards compatibility:
env[CGI::HTTP_HOST] ||= request.authority
if peer = request.peer
env[CGI::REMOTE_ADDR] = peer.ip_address
end
end
def make_environment(request)
Create a base environment hash for the request.
Signature
-
parameter
requestProtocol::HTTP::Request The incoming request.
-
returns
Hash The base environment hash.
Implementation
def make_environment(request)
{
request: request
}
end
def handle_error(env, status, headers, body, error)
Handle errors that occur during request processing. Logs the error, closes any response body, invokes rack.response_finished callbacks, and returns an appropriate failure response.
The rack.response_finished callbacks are invoked in reverse order of registration, as specified by the Rack specification. If a callback raises an exception, it is caught and logged, but does not prevent other callbacks from being invoked.
Signature
-
parameter
envHash The Rack environment hash.
-
parameter
statusInteger | Nil The HTTP status code, if available. May be
nilif the error occurred before the application returned a response.-
parameter
headersHash | Nil The response headers, if available. May be
nilif the error occurred before the application returned a response.-
parameter
bodyObject | Nil The response body, if available. May be
nilif the error occurred before the application returned a response.-
parameter
errorException The exception that occurred during request processing.
-
returns
Protocol::HTTP::Response A failure response representing the error.
Implementation
def handle_error(env, status, headers, body, error)
Console.error(self, "Error occurred during request processing:", error)
# Close the response body if it exists and supports closing:
body&.close if body.respond_to?(:close)
# Invoke `rack.response_finished` callbacks in reverse order of registration. This ensures that callbacks registered later are invoked first, matching the Rack specification.
env&.[](RACK_RESPONSE_FINISHED)&.reverse_each do |callback|
begin
callback.call(env, status, headers, error)
rescue => callback_error
# If a callback raises an exception, log it but continue invoking other callbacks. The Rack specification states that callbacks should not raise exceptions, but we handle this gracefully to prevent one misbehaving callback from breaking others.
Console.error(self, "Error occurred during response finished callback:", callback_error)
end
end
return failure_response(error)
end
def call(request)
Build a Rack env from the incoming request and apply it to the Rack middleware.
Signature
-
parameter
requestProtocol::HTTP::Request The incoming request.
-
returns
Protocol::HTTP::Response The HTTP response.
-
raises
ArgumentError If the status is not an integer or headers are nil.
Implementation
def call(request)
env = self.make_environment(request)
status, headers, body = @app.call(env)
# The status must always be an integer:
unless status.is_a?(Integer)
raise ArgumentError, "Status must be an integer!"
end
# Headers must always be a hash or equivalent:
unless headers
raise ArgumentError, "Headers must not be nil!"
end
headers, meta = self.wrap_headers(headers)
return Response.wrap(env, status, headers, meta, body, request)
rescue => error
return self.handle_error(env, status, headers, body, error)
end
def failure_response(exception)
Generate a suitable response for the given exception.
Signature
-
parameter
exceptionException The exception that occurred.
-
returns
Protocol::HTTP::Response A response representing the error.
Implementation
def failure_response(exception)
Protocol::HTTP::Response.for_exception(exception)
end
def self.extract_protocol(env, response, headers)
Extract protocol information from the environment and response.
Signature
-
parameter
envHash The Rack environment.
-
parameter
responseProtocol::HTTP::Response The HTTP response.
-
parameter
headersHash The response headers to modify.
Implementation
def self.extract_protocol(env, response, headers)
if protocol = response.protocol
# This is the newer mechanism for protocol upgrade:
if env["rack.protocol"]
headers["rack.protocol"] = protocol
# Older mechanism for protocol upgrade:
elsif env[CGI::HTTP_UPGRADE]
headers["upgrade"] = protocol
headers["connection"] = "upgrade"
end
end
end