Class: Protocol::Rack::Adapter::Generic

Inherits:
Object
  • Object
show all
Defined in:
lib/protocol/rack/adapter/generic.rb

Overview

The base adapter class that provides common functionality for all Rack adapters. It handles the conversion between HTTP and Rack environments.

Direct Known Subclasses

Rack2, Rack3

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ Generic

Initialize the rack adaptor middleware.

Raises:

  • (ArgumentError)


41
42
43
44
45
# File 'lib/protocol/rack/adapter/generic.rb', line 41

def initialize(app)
	@app = app
	
	raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call)
end

Class Method Details

.extract_protocol(env, response, headers) ⇒ Object

Extract protocol information from the environment and response.



210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/protocol/rack/adapter/generic.rb', line 210

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

.parse_fileObject

Parses a Rackup file and returns the application.



32
33
34
35
# File 'lib/protocol/rack/adapter/generic.rb', line 32

def self.parse_file(...)
	# This is the old interface, which was changed in Rack 3.
	::Rack::Builder.parse_file(...).first
end

.wrap(app) ⇒ Object

Creates a new adapter instance for the given Rack application. Wraps the adapter in a Rewindable instance to ensure request body can be read multiple times, which is required for Rack < 3.



24
25
26
# File 'lib/protocol/rack/adapter/generic.rb', line 24

def self.wrap(app)
	Rewindable.new(self.new(app))
end

Instance Method Details

#call(request) ⇒ Object

Build a rack ‘env` from the incoming request and apply it to the rack middleware.



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/protocol/rack/adapter/generic.rb', line 175

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

#failure_response(exception) ⇒ Object

Generate a suitable response for the given exception.



201
202
203
# File 'lib/protocol/rack/adapter/generic.rb', line 201

def failure_response(exception)
	Protocol::HTTP::Response.for_exception(exception)
end

#handle_error(env, status, headers, body, error) ⇒ Object

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.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/protocol/rack/adapter/generic.rb', line 148

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

#loggerObject

The logger to use for this adapter.



50
51
52
# File 'lib/protocol/rack/adapter/generic.rb', line 50

def logger
	Console
end

#make_environment(request) ⇒ Object

Create a base environment hash for the request.



132
133
134
135
136
# File 'lib/protocol/rack/adapter/generic.rb', line 132

def make_environment(request)
	{
		request: request
	}
end

#unwrap_headers(headers, env) ⇒ Object

Unwrap HTTP headers into the CGI-style expected by Rack middleware, and add them to the rack ‘env`.

e.g. ‘accept-encoding` becomes `HTTP_ACCEPT_ENCODING`.

Headers keys with underscores will generate the same CGI-style header key as headers with dashes.

e.g ‘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.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/protocol/rack/adapter/generic.rb', line 68

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
		end
	end
end

#unwrap_request(request, env) ⇒ Object

Process the incoming request into a valid rack ‘env`.

  • Set the ‘env` and `env` based on the incoming request body.

  • Set the ‘env` header to the request authority.

  • Set the ‘env` header to the request scheme.

  • Set ‘env` to the request remote adress.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/protocol/rack/adapter/generic.rb', line 93

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
	
	self.unwrap_headers(request.headers, 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