Protocol::RackSourceProtocolRackAdapterGeneric

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 app Interface(: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 path String

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 app Interface(: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 headers Protocol::HTTP::Headers

The raw HTTP request headers.

parameter env Hash

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'] and env['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 request Protocol::HTTP::Request

The incoming request.

parameter env Hash

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 request Protocol::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 env Hash

The Rack environment hash.

parameter status Integer | Nil

The HTTP status code, if available. May be nil if the error occurred before the application returned a response.

parameter headers Hash | Nil

The response headers, if available. May be nil if the error occurred before the application returned a response.

parameter body Object | Nil

The response body, if available. May be nil if the error occurred before the application returned a response.

parameter error Exception

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 request Protocol::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 exception Exception

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 env Hash

The Rack environment.

parameter response Protocol::HTTP::Response

The HTTP response.

parameter headers Hash

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