hyperion-rb 1.6.2 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4768 -0
- data/README.md +222 -13
- data/ext/hyperion_h2_codec/Cargo.lock +7 -0
- data/ext/hyperion_h2_codec/Cargo.toml +33 -0
- data/ext/hyperion_h2_codec/extconf.rb +73 -0
- data/ext/hyperion_h2_codec/src/frames.rs +140 -0
- data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
- data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
- data/ext/hyperion_h2_codec/src/lib.rs +296 -0
- data/ext/hyperion_http/extconf.rb +28 -0
- data/ext/hyperion_http/h2_codec_glue.c +408 -0
- data/ext/hyperion_http/page_cache.c +1125 -0
- data/ext/hyperion_http/parser.c +473 -38
- data/ext/hyperion_http/sendfile.c +982 -0
- data/ext/hyperion_http/websocket.c +493 -0
- data/ext/hyperion_io_uring/Cargo.lock +33 -0
- data/ext/hyperion_io_uring/Cargo.toml +34 -0
- data/ext/hyperion_io_uring/extconf.rb +74 -0
- data/ext/hyperion_io_uring/src/lib.rs +316 -0
- data/lib/hyperion/adapter/rack.rb +370 -42
- data/lib/hyperion/admin_listener.rb +207 -0
- data/lib/hyperion/admin_middleware.rb +36 -7
- data/lib/hyperion/cli.rb +310 -11
- data/lib/hyperion/config.rb +440 -14
- data/lib/hyperion/connection.rb +679 -22
- data/lib/hyperion/deprecations.rb +81 -0
- data/lib/hyperion/dispatch_mode.rb +165 -0
- data/lib/hyperion/fiber_local.rb +75 -13
- data/lib/hyperion/h2_admission.rb +77 -0
- data/lib/hyperion/h2_codec.rb +499 -0
- data/lib/hyperion/http/page_cache.rb +122 -0
- data/lib/hyperion/http/sendfile.rb +696 -0
- data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
- data/lib/hyperion/http2_handler.rb +618 -19
- data/lib/hyperion/io_uring.rb +317 -0
- data/lib/hyperion/lint_wrapper_pool.rb +126 -0
- data/lib/hyperion/master.rb +96 -9
- data/lib/hyperion/metrics/path_templater.rb +68 -0
- data/lib/hyperion/metrics.rb +256 -0
- data/lib/hyperion/prometheus_exporter.rb +150 -0
- data/lib/hyperion/request.rb +13 -0
- data/lib/hyperion/response_writer.rb +477 -16
- data/lib/hyperion/runtime.rb +195 -0
- data/lib/hyperion/server/route_table.rb +179 -0
- data/lib/hyperion/server.rb +519 -55
- data/lib/hyperion/static_preload.rb +133 -0
- data/lib/hyperion/thread_pool.rb +61 -7
- data/lib/hyperion/tls.rb +343 -1
- data/lib/hyperion/version.rb +1 -1
- data/lib/hyperion/websocket/close_codes.rb +71 -0
- data/lib/hyperion/websocket/connection.rb +876 -0
- data/lib/hyperion/websocket/frame.rb +356 -0
- data/lib/hyperion/websocket/handshake.rb +525 -0
- data/lib/hyperion/worker.rb +111 -9
- data/lib/hyperion.rb +137 -3
- metadata +50 -1
|
@@ -3,47 +3,111 @@
|
|
|
3
3
|
require 'stringio'
|
|
4
4
|
require_relative '../version'
|
|
5
5
|
require_relative '../pool'
|
|
6
|
+
require_relative '../websocket/handshake'
|
|
6
7
|
|
|
7
8
|
module Hyperion
|
|
8
9
|
module Adapter
|
|
9
10
|
# NOTE: this is Hyperion::Adapter::Rack, not the Rack gem.
|
|
10
11
|
# Reference the Rack gem as ::Rack inside this module if needed.
|
|
11
12
|
module Rack
|
|
12
|
-
# Pre-frozen mapping for the
|
|
13
|
+
# Pre-frozen mapping for the 30 most common HTTP request headers.
|
|
13
14
|
# Skips the per-request `"HTTP_#{name.upcase.tr('-', '_')}"` allocation
|
|
14
15
|
# (5–15 string ops per request × N headers). Uncached header names fall
|
|
15
16
|
# back to the dynamic computation. Keys are lowercased to match the
|
|
16
17
|
# parser's normalisation.
|
|
18
|
+
#
|
|
19
|
+
# Phase 2c (1.7.1) widened from 16 to 30 entries to cover the full
|
|
20
|
+
# production-traffic top-30 (Sec-Fetch-*, X-Forwarded-Host, X-Real-IP,
|
|
21
|
+
# If-None-Match, etc.). When the C extension is built, the values
|
|
22
|
+
# below are replaced with the *same* frozen VALUEs registered by
|
|
23
|
+
# CParser::PREINTERNED_HEADERS, so the env hash, parser, and adapter
|
|
24
|
+
# all share string identity for these keys (`#equal?` is true). This
|
|
25
|
+
# is what unlocks the spec assertion that `env['HTTP_USER_AGENT']` key
|
|
26
|
+
# is `equal?` to the pre-interned key — and it lets downstream Rack
|
|
27
|
+
# apps that key into env via these same literal strings hit a
|
|
28
|
+
# GVAR-backed pointer compare instead of a hash byte compare.
|
|
17
29
|
HTTP_KEY_CACHE = {
|
|
18
30
|
'host' => 'HTTP_HOST',
|
|
19
31
|
'user-agent' => 'HTTP_USER_AGENT',
|
|
20
32
|
'accept' => 'HTTP_ACCEPT',
|
|
21
33
|
'accept-encoding' => 'HTTP_ACCEPT_ENCODING',
|
|
22
34
|
'accept-language' => 'HTTP_ACCEPT_LANGUAGE',
|
|
35
|
+
'cache-control' => 'HTTP_CACHE_CONTROL',
|
|
23
36
|
'connection' => 'HTTP_CONNECTION',
|
|
24
|
-
'content-type' => 'HTTP_CONTENT_TYPE',
|
|
25
|
-
'content-length' => 'HTTP_CONTENT_LENGTH',
|
|
26
37
|
'cookie' => 'HTTP_COOKIE',
|
|
38
|
+
'content-length' => 'HTTP_CONTENT_LENGTH',
|
|
39
|
+
'content-type' => 'HTTP_CONTENT_TYPE',
|
|
27
40
|
'authorization' => 'HTTP_AUTHORIZATION',
|
|
28
|
-
'cache-control' => 'HTTP_CACHE_CONTROL',
|
|
29
41
|
'referer' => 'HTTP_REFERER',
|
|
30
42
|
'origin' => 'HTTP_ORIGIN',
|
|
43
|
+
'upgrade' => 'HTTP_UPGRADE',
|
|
31
44
|
'x-forwarded-for' => 'HTTP_X_FORWARDED_FOR',
|
|
32
45
|
'x-forwarded-proto' => 'HTTP_X_FORWARDED_PROTO',
|
|
33
|
-
'x-
|
|
46
|
+
'x-forwarded-host' => 'HTTP_X_FORWARDED_HOST',
|
|
47
|
+
'x-real-ip' => 'HTTP_X_REAL_IP',
|
|
48
|
+
'x-request-id' => 'HTTP_X_REQUEST_ID',
|
|
49
|
+
'if-none-match' => 'HTTP_IF_NONE_MATCH',
|
|
50
|
+
'if-modified-since' => 'HTTP_IF_MODIFIED_SINCE',
|
|
51
|
+
'if-match' => 'HTTP_IF_MATCH',
|
|
52
|
+
'etag' => 'HTTP_ETAG',
|
|
53
|
+
'range' => 'HTTP_RANGE',
|
|
54
|
+
'pragma' => 'HTTP_PRAGMA',
|
|
55
|
+
'dnt' => 'HTTP_DNT',
|
|
56
|
+
'sec-ch-ua' => 'HTTP_SEC_CH_UA',
|
|
57
|
+
'sec-fetch-dest' => 'HTTP_SEC_FETCH_DEST',
|
|
58
|
+
'sec-fetch-mode' => 'HTTP_SEC_FETCH_MODE',
|
|
59
|
+
'sec-fetch-site' => 'HTTP_SEC_FETCH_SITE'
|
|
34
60
|
}.freeze
|
|
35
61
|
|
|
62
|
+
# If the C extension is loaded, rebind HTTP_KEY_CACHE values to the
|
|
63
|
+
# *same* frozen VALUEs the parser registers in CParser::PREINTERNED_HEADERS.
|
|
64
|
+
# This collapses three otherwise-distinct frozen Strings ("HTTP_HOST" in
|
|
65
|
+
# this Hash, "HTTP_HOST" in the env, "HTTP_HOST" the parser interned) into
|
|
66
|
+
# one shared object — `equal?` becomes pointer-compare for downstream
|
|
67
|
+
# consumers. The mutation runs once at load time, before the constant is
|
|
68
|
+
# observed externally; from that point on the Hash itself is frozen.
|
|
69
|
+
if defined?(::Hyperion::CParser) &&
|
|
70
|
+
::Hyperion::CParser.const_defined?(:PREINTERNED_HEADERS)
|
|
71
|
+
pairs = ::Hyperion::CParser::PREINTERNED_HEADERS
|
|
72
|
+
# Walk the parallel [lc, http_key, lc, http_key, ...] flat array.
|
|
73
|
+
unfrozen = HTTP_KEY_CACHE.dup
|
|
74
|
+
i = 0
|
|
75
|
+
while i < pairs.length
|
|
76
|
+
lc = pairs[i]
|
|
77
|
+
http_key = pairs[i + 1]
|
|
78
|
+
unfrozen[lc] = http_key if unfrozen.key?(lc)
|
|
79
|
+
i += 2
|
|
80
|
+
end
|
|
81
|
+
remove_const(:HTTP_KEY_CACHE)
|
|
82
|
+
HTTP_KEY_CACHE = unfrozen.freeze
|
|
83
|
+
end
|
|
84
|
+
|
|
36
85
|
ENV_POOL = Hyperion::Pool.new(
|
|
37
86
|
max_size: 256,
|
|
38
87
|
factory: -> { {} },
|
|
39
88
|
reset: ->(env) { env.clear }
|
|
40
89
|
)
|
|
41
90
|
|
|
91
|
+
# Phase 11 — shared frozen empty buffer for StringIO reset. Pre-Phase-11
|
|
92
|
+
# the reset lambda allocated a fresh `+''` per request (one String per
|
|
93
|
+
# acquire). The next call to `build_env` immediately swaps in
|
|
94
|
+
# `input.string = request.body`, so we never mutate this buffer — a
|
|
95
|
+
# single frozen empty String is sufficient as a "clean slate" sentinel.
|
|
96
|
+
EMPTY_INPUT_BUFFER = String.new('', encoding: Encoding::ASCII_8BIT).freeze
|
|
97
|
+
|
|
98
|
+
# Phase 11 — frozen literal constants for env values that pre-Phase-11
|
|
99
|
+
# were rebuilt per request:
|
|
100
|
+
# * SERVER_SOFTWARE — `"Hyperion/#{VERSION}"` interpolated each call.
|
|
101
|
+
# * RACK_VERSION — `[3, 0]` Array literal allocated each call.
|
|
102
|
+
# The Array is frozen so Rack apps can't mutate the shared instance.
|
|
103
|
+
SERVER_SOFTWARE_VALUE = "Hyperion/#{Hyperion::VERSION}".freeze
|
|
104
|
+
RACK_VERSION = [3, 0].freeze
|
|
105
|
+
|
|
42
106
|
INPUT_POOL = Hyperion::Pool.new(
|
|
43
107
|
max_size: 256,
|
|
44
108
|
factory: -> { StringIO.new },
|
|
45
109
|
reset: lambda { |io|
|
|
46
|
-
io.string =
|
|
110
|
+
io.string = EMPTY_INPUT_BUFFER
|
|
47
111
|
io.rewind
|
|
48
112
|
}
|
|
49
113
|
)
|
|
@@ -59,6 +123,19 @@ module Hyperion
|
|
|
59
123
|
::Hyperion::CParser.respond_to?(:upcase_underscore)
|
|
60
124
|
end
|
|
61
125
|
|
|
126
|
+
# Phase 3a (1.7.1) — whether the full env-build loop has moved into C.
|
|
127
|
+
# When true, build_env hands the populated env Hash + Request to the
|
|
128
|
+
# C ext, which sets REQUEST_METHOD / PATH_INFO / QUERY_STRING /
|
|
129
|
+
# HTTP_VERSION / SERVER_PROTOCOL / CONTENT_TYPE / CONTENT_LENGTH +
|
|
130
|
+
# every HTTP_<UPCASED> header in one trip across the FFI boundary.
|
|
131
|
+
# The Ruby fallback below stays exercised by spec for parity coverage.
|
|
132
|
+
def self.c_build_env_available?
|
|
133
|
+
return @c_build_env_available unless @c_build_env_available.nil?
|
|
134
|
+
|
|
135
|
+
@c_build_env_available = defined?(::Hyperion::CParser) &&
|
|
136
|
+
::Hyperion::CParser.respond_to?(:build_env)
|
|
137
|
+
end
|
|
138
|
+
|
|
62
139
|
class << self
|
|
63
140
|
# Pre-allocate `n` env-hash and rack-input objects in master before
|
|
64
141
|
# fork. Children inherit the populated free-list via copy-on-write —
|
|
@@ -74,10 +151,90 @@ module Hyperion
|
|
|
74
151
|
nil
|
|
75
152
|
end
|
|
76
153
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
154
|
+
# 2.1.0 (WS-1): `connection:` is the Hyperion::Connection that owns
|
|
155
|
+
# the underlying socket for this request. When non-nil, the env hash
|
|
156
|
+
# advertises Rack 3 full-hijack support — the app can call
|
|
157
|
+
# `env['rack.hijack'].call` to take ownership of the raw socket and
|
|
158
|
+
# speak any post-HTTP protocol (WebSocket, raw TCP tunnel, etc.).
|
|
159
|
+
# When nil (HTTP/2 path, ad-hoc adapter callers in specs), hijack
|
|
160
|
+
# stays disabled — `env['rack.hijack?']` returns false and the env
|
|
161
|
+
# has no `rack.hijack` key, matching pre-2.1 behaviour.
|
|
162
|
+
#
|
|
163
|
+
# 2.5-C: `runtime:` is the Hyperion::Runtime that owns this
|
|
164
|
+
# request's metrics + logger + lifecycle hooks. When nil (the
|
|
165
|
+
# default — every existing in-tree call site stays untouched),
|
|
166
|
+
# the adapter resolves to `Hyperion::Runtime.default`, which is
|
|
167
|
+
# the same singleton legacy `Hyperion.metrics` / `Hyperion.logger`
|
|
168
|
+
# delegate to. Apps with multiple servers (multi-tenant) pass an
|
|
169
|
+
# explicit Runtime so each server's NewRelic / AppSignal /
|
|
170
|
+
# OpenTelemetry hooks remain isolated.
|
|
171
|
+
def call(app, request, connection: nil, runtime: nil)
|
|
172
|
+
env, input = build_env(request, connection: connection)
|
|
173
|
+
|
|
174
|
+
# 2.1.0 (WS-2) — RFC 6455 §4.2 handshake interception. Runs
|
|
175
|
+
# AFTER env is built (so the WS module sees the same env keys
|
|
176
|
+
# the app would see) but BEFORE app.call. Branches:
|
|
177
|
+
# * :not_websocket — request is plain HTTP; no-op
|
|
178
|
+
# * :ok — valid WS handshake; stash the
|
|
179
|
+
# [:ok, accept, subprotocol] tuple in env so the app can
|
|
180
|
+
# read accept_value without re-running SHA1/base64. The
|
|
181
|
+
# app is still responsible for writing the 101 response
|
|
182
|
+
# to the hijacked socket (Option B from the WS-2 plan,
|
|
183
|
+
# mirrors faye-websocket / ActionCable convention).
|
|
184
|
+
# * :bad_request / :upgrade_required — short-circuit; the
|
|
185
|
+
# app never sees the env. Hyperion writes the 4xx itself.
|
|
186
|
+
ws_result = Hyperion::WebSocket::Handshake.validate(env)
|
|
187
|
+
case ws_result.first
|
|
188
|
+
when :ok
|
|
189
|
+
env['hyperion.websocket.handshake'] = ws_result
|
|
190
|
+
when :bad_request, :upgrade_required
|
|
191
|
+
return websocket_handshake_failure_response(ws_result)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# 2.5-C — per-request lifecycle hooks. The `has_request_hooks?`
|
|
195
|
+
# guard collapses to two empty-Array checks when no observers
|
|
196
|
+
# are registered (the default for every Hyperion install that
|
|
197
|
+
# hasn't opted in), so the hot path stays allocation-free
|
|
198
|
+
# — verified by `yjit_alloc_audit_spec`. Resolving `runtime`
|
|
199
|
+
# is itself zero-allocation: `Runtime.default` returns a
|
|
200
|
+
# memoised singleton.
|
|
201
|
+
#
|
|
202
|
+
# 2.6-D — when the response auto-detects into `:inline_blocking`
|
|
203
|
+
# (static-file body responding to `:to_path`, no streaming
|
|
204
|
+
# marker) we SKIP the after-request lifecycle hook. Static
|
|
205
|
+
# asset traffic is high-volume + low-value for trace
|
|
206
|
+
# instrumentation: a NewRelic / OpenTelemetry hook firing on
|
|
207
|
+
# every 200-byte favicon or 1 MiB asset response wastes CPU
|
|
208
|
+
# finishing spans nobody queries. Operators wanting to
|
|
209
|
+
# observe static traffic should use the metrics module
|
|
210
|
+
# (per-route histogram + sendfile_responses counter), which
|
|
211
|
+
# is allocation-free on the hot path. The before-request
|
|
212
|
+
# hook still fires — its semantic ("the request is about
|
|
213
|
+
# to be processed") is preserved across all dispatch modes;
|
|
214
|
+
# it's the after-hook (typically heavy: span flush, DB
|
|
215
|
+
# write, async-queue enqueue) that benefits from the skip.
|
|
216
|
+
rt = runtime || Hyperion::Runtime.default
|
|
217
|
+
if rt.has_request_hooks?
|
|
218
|
+
rt.fire_request_start(request, env)
|
|
219
|
+
begin
|
|
220
|
+
response = app.call(env)
|
|
221
|
+
rescue StandardError => e
|
|
222
|
+
rt.fire_request_end(request, env, nil, e)
|
|
223
|
+
raise
|
|
224
|
+
end
|
|
225
|
+
resolve_dispatch_mode!(env, response, connection)
|
|
226
|
+
rt.fire_request_end(request, env, response, nil) unless inline_blocking_resolved?(connection)
|
|
227
|
+
return response
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Phase 11 — return the app's tuple directly. Pre-Phase-11
|
|
231
|
+
# destructured it into 3 locals and re-built the [status, headers,
|
|
232
|
+
# body] Array (one Array allocation per request). Apps already
|
|
233
|
+
# return a [status, headers, body] triple per Rack spec, so the
|
|
234
|
+
# rebuild is pure overhead.
|
|
235
|
+
response = app.call(env)
|
|
236
|
+
resolve_dispatch_mode!(env, response, connection)
|
|
237
|
+
response
|
|
81
238
|
rescue StandardError => e
|
|
82
239
|
Hyperion.metrics.increment(:app_errors)
|
|
83
240
|
Hyperion.logger.error do
|
|
@@ -95,13 +252,112 @@ module Hyperion
|
|
|
95
252
|
# is iterated lazily — release happens after the writer.
|
|
96
253
|
# For Phase 5 simplicity we release synchronously since the writer
|
|
97
254
|
# buffers fully. Phase 7 (HTTP/2 streaming) will revisit.
|
|
98
|
-
|
|
99
|
-
|
|
255
|
+
#
|
|
256
|
+
# 2.1.0 hijack: when the app full-hijacked the socket, the env
|
|
257
|
+
# references (notably the rack.hijack proc + buffered carry) are
|
|
258
|
+
# *the* live reference to the connection. Returning the env to the
|
|
259
|
+
# pool here would let a subsequent request reuse the same hash and
|
|
260
|
+
# silently null out the hijacker's state. Skip the pool release on
|
|
261
|
+
# hijacked connections and let the hash be GC'd normally.
|
|
262
|
+
if env && connection && connection.respond_to?(:hijacked?) && connection.hijacked?
|
|
263
|
+
# Drop the input back into the pool (it's a fresh StringIO and
|
|
264
|
+
# the hijacker doesn't reference it). Skip env recycling.
|
|
265
|
+
INPUT_POOL.release(input) if input
|
|
266
|
+
else
|
|
267
|
+
ENV_POOL.release(env) if env
|
|
268
|
+
INPUT_POOL.release(input) if input
|
|
269
|
+
end
|
|
100
270
|
end
|
|
101
271
|
|
|
102
272
|
private
|
|
103
273
|
|
|
104
|
-
|
|
274
|
+
# 2.1.0 (WS-2) — translate a Handshake.validate failure tuple
|
|
275
|
+
# into a Rack response triple. 400 for protocol errors,
|
|
276
|
+
# 426 for unsupported Sec-WebSocket-Version (RFC 6455 §4.4)
|
|
277
|
+
# — the 426 path always carries `sec-websocket-version: 13`
|
|
278
|
+
# so the client sees the version Hyperion supports.
|
|
279
|
+
def websocket_handshake_failure_response(ws_result)
|
|
280
|
+
tag, body, extra_headers = ws_result
|
|
281
|
+
status = tag == :upgrade_required ? 426 : 400
|
|
282
|
+
response_headers = { 'content-type' => 'text/plain' }
|
|
283
|
+
extra_headers.each { |k, v| response_headers[k.to_s] = v.to_s }
|
|
284
|
+
[status, response_headers, [body.to_s]]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# 2.6-C — resolve the per-response dispatch-mode override and
|
|
288
|
+
# stash it on the connection so `Connection#serve` can forward
|
|
289
|
+
# it to `ResponseWriter#write`. Two opt-in mechanisms, in
|
|
290
|
+
# priority order:
|
|
291
|
+
#
|
|
292
|
+
# 1. Explicit: the Rack app set
|
|
293
|
+
# `env['hyperion.dispatch_mode'] = :inline_blocking` (or
|
|
294
|
+
# another future symbol). Operator-level escape hatch
|
|
295
|
+
# for routes the auto-detect doesn't catch (e.g. a custom
|
|
296
|
+
# lazy-streaming body that responds to `:to_path` for
|
|
297
|
+
# Range-request reasons but is logically a streaming
|
|
298
|
+
# response). We honour any explicit non-nil value
|
|
299
|
+
# verbatim; the writer's branch is keyed on the symbol
|
|
300
|
+
# so unknown symbols fall through to the default fiber-
|
|
301
|
+
# yielding path.
|
|
302
|
+
#
|
|
303
|
+
# 2. Auto-detect: response body responds to `:to_path` AND
|
|
304
|
+
# `env['hyperion.streaming']` is not set. `to_path` is
|
|
305
|
+
# Rack's strongest "this is a static file on disk"
|
|
306
|
+
# signal — Rack::Files, Rack::SendFile, asset servers,
|
|
307
|
+
# and signed-download responders all set it; streaming
|
|
308
|
+
# bodies (SSE, JSON streams, chunked Enumerators) do not.
|
|
309
|
+
# The `hyperion.streaming` env key is the operator's
|
|
310
|
+
# escape valve to opt OUT of the auto-detect (e.g. a
|
|
311
|
+
# custom Range-request body that responds to `:to_path`
|
|
312
|
+
# but should still take the fiber-yielding path because
|
|
313
|
+
# the body itself does I/O wait between chunks).
|
|
314
|
+
#
|
|
315
|
+
# Conservative-by-design: if `connection` is nil (no socket-
|
|
316
|
+
# owning Connection in scope — h2 streams, ad-hoc adapter
|
|
317
|
+
# callers in specs), we skip the override entirely. The
|
|
318
|
+
# h2 path has its own per-stream fiber dispatch and doesn't
|
|
319
|
+
# benefit from `:inline_blocking`.
|
|
320
|
+
#
|
|
321
|
+
# Skips a bad response shape (anything that's not a 3-element
|
|
322
|
+
# Array) gracefully — the caller's main rescue clause owns
|
|
323
|
+
# malformed-response handling.
|
|
324
|
+
def resolve_dispatch_mode!(env, response, connection)
|
|
325
|
+
return unless connection
|
|
326
|
+
return unless connection.respond_to?(:response_dispatch_mode=)
|
|
327
|
+
return unless response.is_a?(Array) && response.length == 3
|
|
328
|
+
|
|
329
|
+
# 1. Explicit opt-in via env wins.
|
|
330
|
+
explicit = env && env['hyperion.dispatch_mode']
|
|
331
|
+
if explicit
|
|
332
|
+
connection.response_dispatch_mode = explicit
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# 2. Auto-detect on `to_path` static-file responses. Skip
|
|
337
|
+
# when the app set `hyperion.streaming` — that's the
|
|
338
|
+
# operator's "this body responds to to_path but is
|
|
339
|
+
# logically a streaming response" escape valve.
|
|
340
|
+
return if env && env['hyperion.streaming']
|
|
341
|
+
|
|
342
|
+
body = response[2]
|
|
343
|
+
return unless body.respond_to?(:to_path)
|
|
344
|
+
|
|
345
|
+
connection.response_dispatch_mode = :inline_blocking
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# 2.6-D — read-back of the resolved dispatch mode. Used by the
|
|
349
|
+
# lifecycle-hook branch in `#call` to decide whether to fire
|
|
350
|
+
# the after-request hook. Returns false when `connection` is
|
|
351
|
+
# nil (h2 streams, ad-hoc adapter callers in specs) so those
|
|
352
|
+
# paths keep their hook firing behaviour unchanged.
|
|
353
|
+
def inline_blocking_resolved?(connection)
|
|
354
|
+
return false unless connection
|
|
355
|
+
return false unless connection.respond_to?(:response_dispatch_mode)
|
|
356
|
+
|
|
357
|
+
connection.response_dispatch_mode == :inline_blocking
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def build_env(request, connection: nil)
|
|
105
361
|
host_header = request.header('host') || ''
|
|
106
362
|
server_name, server_port = split_host(host_header)
|
|
107
363
|
|
|
@@ -110,14 +366,13 @@ module Hyperion
|
|
|
110
366
|
input.string = request.body
|
|
111
367
|
input.rewind
|
|
112
368
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
369
|
+
# Adapter-owned (non-header, non-request-line) env. SERVER_NAME/PORT
|
|
370
|
+
# need split_host, REMOTE_ADDR needs peer info, the rack.* keys are
|
|
371
|
+
# constants — none of these benefit from the FFI hop, so they stay
|
|
372
|
+
# in Ruby regardless of c_build_env_available?.
|
|
116
373
|
env['SERVER_NAME'] = server_name
|
|
117
374
|
env['SERVER_PORT'] = server_port
|
|
118
|
-
env['
|
|
119
|
-
env['HTTP_VERSION'] = request.http_version
|
|
120
|
-
env['SERVER_SOFTWARE'] = "Hyperion/#{Hyperion::VERSION}"
|
|
375
|
+
env['SERVER_SOFTWARE'] = SERVER_SOFTWARE_VALUE
|
|
121
376
|
# Rack apps (Rack::Attack throttles, IpHelper.real_ip, audit logging)
|
|
122
377
|
# require REMOTE_ADDR. Fall back to localhost when no peer info is
|
|
123
378
|
# available — typically when a Request is constructed in specs
|
|
@@ -126,32 +381,88 @@ module Hyperion
|
|
|
126
381
|
env['rack.url_scheme'] = 'http'
|
|
127
382
|
env['rack.input'] = input
|
|
128
383
|
env['rack.errors'] = $stderr
|
|
129
|
-
|
|
130
|
-
|
|
384
|
+
if connection
|
|
385
|
+
# 2.1.0 (WS-1) — Rack 3 full-hijack. The proc captures the
|
|
386
|
+
# connection (NOT the socket directly) so the connection can
|
|
387
|
+
# flip its @hijacked flag synchronously inside hijack!; that
|
|
388
|
+
# way the writer / cleanup paths see the flag the moment the
|
|
389
|
+
# app takes over the wire. The proc returns the raw socket
|
|
390
|
+
# IO, per Rack 3 spec.
|
|
391
|
+
env['rack.hijack?'] = true
|
|
392
|
+
env['rack.hijack'] = lambda do
|
|
393
|
+
connection.hijack!
|
|
394
|
+
end
|
|
395
|
+
# Hyperion-specific extension: any bytes the connection had
|
|
396
|
+
# buffered past the parsed request (pipelined/keep-alive
|
|
397
|
+
# carry, or — for an Upgrade — bytes the client sent
|
|
398
|
+
# immediately after the headers). Empty string when none.
|
|
399
|
+
# The app reads these BEFORE reading from the hijacked
|
|
400
|
+
# socket. Documented in CHANGELOG; not a Rack 3 spec key.
|
|
401
|
+
env['hyperion.hijack_buffered'] =
|
|
402
|
+
connection.respond_to?(:hijack_buffered) ? connection.hijack_buffered : +''
|
|
403
|
+
else
|
|
404
|
+
env['rack.hijack?'] = false
|
|
405
|
+
end
|
|
406
|
+
env['rack.version'] = RACK_VERSION
|
|
131
407
|
env['rack.multithread'] = false
|
|
132
408
|
env['rack.multiprocess'] = false
|
|
133
409
|
env['rack.run_once'] = false
|
|
134
410
|
env['SCRIPT_NAME'] = ''
|
|
135
411
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
env[
|
|
145
|
-
|
|
412
|
+
if Rack.c_build_env_available?
|
|
413
|
+
# Phase 3a (1.7.1) — single FFI call sets REQUEST_METHOD,
|
|
414
|
+
# PATH_INFO, QUERY_STRING, HTTP_VERSION, SERVER_PROTOCOL,
|
|
415
|
+
# CONTENT_TYPE, CONTENT_LENGTH, and every HTTP_* header.
|
|
416
|
+
::Hyperion::CParser.build_env(env, request)
|
|
417
|
+
else
|
|
418
|
+
env['REQUEST_METHOD'] = request.method
|
|
419
|
+
env['PATH_INFO'] = request.path
|
|
420
|
+
env['QUERY_STRING'] = request.query_string
|
|
421
|
+
env['SERVER_PROTOCOL'] = request.http_version
|
|
422
|
+
env['HTTP_VERSION'] = request.http_version
|
|
146
423
|
|
|
147
|
-
|
|
148
|
-
|
|
424
|
+
# Header-name → Rack env-key conversion. Cache covers the
|
|
425
|
+
# 30 most common names; uncached headers (X-* customs,
|
|
426
|
+
# vendor-specific) flow through CParser.upcase_underscore
|
|
427
|
+
# (single C-level allocation) when the ext is built, else
|
|
428
|
+
# the pure-Ruby triple-allocation path.
|
|
429
|
+
c_upcase = Rack.c_upcase_available?
|
|
430
|
+
request.headers.each do |name, value|
|
|
431
|
+
key = HTTP_KEY_CACHE[name] ||
|
|
432
|
+
(c_upcase ? ::Hyperion::CParser.upcase_underscore(name) : "HTTP_#{name.upcase.tr('-', '_')}")
|
|
433
|
+
env[key] = value
|
|
434
|
+
end
|
|
149
435
|
|
|
150
|
-
|
|
436
|
+
env['CONTENT_TYPE'] = env['HTTP_CONTENT_TYPE'] if env.key?('HTTP_CONTENT_TYPE')
|
|
437
|
+
env['CONTENT_LENGTH'] = env['HTTP_CONTENT_LENGTH'] if env.key?('HTTP_CONTENT_LENGTH')
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Phase 11 — reuse a per-thread 2-element scratch Array for the
|
|
441
|
+
# `env, input = build_env(...)` destructuring return. Pre-Phase-11
|
|
442
|
+
# allocated a fresh `[env, input]` Array per request; the caller
|
|
443
|
+
# destructures immediately and never holds onto the Array, so a
|
|
444
|
+
# mutable per-thread tuple is safe (each request runs to the
|
|
445
|
+
# destructure on the same thread before any nested build_env call
|
|
446
|
+
# could observe it).
|
|
447
|
+
tup = (Thread.current[:__hyperion_build_env_tuple__] ||= [nil, nil])
|
|
448
|
+
tup[0] = env
|
|
449
|
+
tup[1] = input
|
|
450
|
+
tup
|
|
151
451
|
end
|
|
152
452
|
|
|
453
|
+
# Phase 11 — frozen "no port specified" sentinels so the
|
|
454
|
+
# overwhelmingly common host_header == "host" / "host:80" /
|
|
455
|
+
# "host:443" branches don't allocate a fresh Array on every
|
|
456
|
+
# request. The caller destructures into `server_name, server_port =
|
|
457
|
+
# split_host(...)` immediately and never holds the returned Array,
|
|
458
|
+
# so a per-thread mutable scratch Array is safe (and the frozen
|
|
459
|
+
# `LOCALHOST_DEFAULTS` sentinel covers the empty-host cases without
|
|
460
|
+
# any allocation at all).
|
|
461
|
+
LOCALHOST_DEFAULTS = %w[localhost 80].freeze
|
|
462
|
+
DEFAULT_PORT_80 = '80'
|
|
463
|
+
|
|
153
464
|
def split_host(host_header)
|
|
154
|
-
return
|
|
465
|
+
return LOCALHOST_DEFAULTS if host_header.empty?
|
|
155
466
|
|
|
156
467
|
if host_header.start_with?('[')
|
|
157
468
|
close = host_header.index(']')
|
|
@@ -164,20 +475,37 @@ module Hyperion
|
|
|
164
475
|
# on header-parse failures, so we degrade gracefully instead.
|
|
165
476
|
unless close
|
|
166
477
|
Hyperion.metrics.increment(:malformed_host_header)
|
|
167
|
-
return
|
|
478
|
+
return LOCALHOST_DEFAULTS
|
|
168
479
|
end
|
|
169
480
|
|
|
170
481
|
name = host_header[0..close]
|
|
171
482
|
rest = host_header[(close + 1)..]
|
|
172
|
-
port = rest&.start_with?(':') ? rest[1..] :
|
|
173
|
-
|
|
174
|
-
elsif host_header.
|
|
175
|
-
|
|
176
|
-
[name,
|
|
483
|
+
port = rest&.start_with?(':') ? rest[1..] : DEFAULT_PORT_80
|
|
484
|
+
split_host_tuple(name, port.to_s.empty? ? DEFAULT_PORT_80 : port)
|
|
485
|
+
elsif (idx = host_header.index(':'))
|
|
486
|
+
# Phase 11 — replace `split(':', 2)` (allocates 2 Strings + 1
|
|
487
|
+
# transient Array that's then discarded for a fresh `[name,
|
|
488
|
+
# port]` literal). Hand-rolled byteslice keeps the 2 substring
|
|
489
|
+
# allocations (unavoidable — the env hash retains them) but
|
|
490
|
+
# routes the surrounding container through the per-thread
|
|
491
|
+
# scratch tuple, dropping 1 Array allocation per request.
|
|
492
|
+
split_host_tuple(host_header.byteslice(0, idx),
|
|
493
|
+
host_header.byteslice(idx + 1, host_header.bytesize - idx - 1))
|
|
177
494
|
else
|
|
178
|
-
|
|
495
|
+
split_host_tuple(host_header, DEFAULT_PORT_80)
|
|
179
496
|
end
|
|
180
497
|
end
|
|
498
|
+
|
|
499
|
+
# Per-thread 2-element scratch Array for split_host's return tuple.
|
|
500
|
+
# See note on `__hyperion_build_env_tuple__` in build_env — the
|
|
501
|
+
# caller destructures immediately; no nested split_host call can
|
|
502
|
+
# observe the same thread's tuple before the destructure completes.
|
|
503
|
+
def split_host_tuple(name, port)
|
|
504
|
+
tup = (Thread.current[:__hyperion_split_host_tuple__] ||= [nil, nil])
|
|
505
|
+
tup[0] = name
|
|
506
|
+
tup[1] = port
|
|
507
|
+
tup
|
|
508
|
+
end
|
|
181
509
|
end
|
|
182
510
|
end
|
|
183
511
|
end
|