raptor 0.12.0 → 0.13.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 +4 -0
- data/README.md +10 -10
- data/docs/raptor-vs-puma.md +29 -29
- data/ext/raptor_bpf/reuseport_select.bpf.c +8 -4
- data/lib/raptor/binder.rb +37 -87
- data/lib/raptor/cli.rb +9 -25
- data/lib/raptor/cluster.rb +17 -53
- data/lib/raptor/http.rb +18 -0
- data/lib/raptor/http1.rb +244 -233
- data/lib/raptor/http2.rb +42 -63
- data/lib/raptor/reactor.rb +21 -52
- data/lib/raptor/reuseport_bpf.rb +14 -5
- data/lib/raptor/server.rb +10 -36
- data/lib/raptor/stats.rb +3 -8
- data/lib/raptor/systemd.rb +1 -1
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +34 -80
- data/sig/generated/raptor/cli.rbs +9 -25
- data/sig/generated/raptor/cluster.rbs +10 -51
- data/sig/generated/raptor/http.rbs +10 -0
- data/sig/generated/raptor/http1.rbs +129 -113
- data/sig/generated/raptor/http2.rbs +31 -48
- data/sig/generated/raptor/reactor.rbs +21 -52
- data/sig/generated/raptor/reuseport_bpf.rbs +11 -2
- data/sig/generated/raptor/server.rbs +9 -35
- data/sig/generated/raptor/stats.rbs +3 -8
- metadata +1 -1
data/lib/raptor/http1.rb
CHANGED
|
@@ -155,6 +155,69 @@ module Raptor
|
|
|
155
155
|
[decoded, :incomplete]
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
+
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
159
|
+
# bytes. A complete well-formed request returns with `:complete` set
|
|
160
|
+
# plus populated `:env` and `:body`; malformed or oversized input flips
|
|
161
|
+
# `:malformed` or `:too_large`; incomplete input returns the state
|
|
162
|
+
# so the reactor can wait for more.
|
|
163
|
+
#
|
|
164
|
+
# @param data [Hash] the current parse state
|
|
165
|
+
# @param env_template [Hash] the Rack env template to seed the request with
|
|
166
|
+
# @param max_body_size [Integer, nil] byte limit for the request body, or nil for no limit
|
|
167
|
+
# @return [Hash] the updated parse state, made shareable for cross-Ractor return
|
|
168
|
+
#
|
|
169
|
+
# @rbs (Hash[Symbol, untyped] data, Hash[String, untyped] env_template, Integer? max_body_size) -> Hash[Symbol, untyped]
|
|
170
|
+
def self.parse(data, env_template, max_body_size)
|
|
171
|
+
parser = Raptor::HttpParser.new
|
|
172
|
+
env = env_template.dup
|
|
173
|
+
nread = begin
|
|
174
|
+
parser.execute(env, data[:buffer], 0)
|
|
175
|
+
rescue Raptor::HttpParserError
|
|
176
|
+
return Ractor.make_shareable(data.merge(complete: true, malformed: true))
|
|
177
|
+
end
|
|
178
|
+
parse_data = if data[:parse_data]
|
|
179
|
+
data[:parse_data].dup
|
|
180
|
+
else
|
|
181
|
+
{ parse_count: 0, content_length: parser.content_length }
|
|
182
|
+
end
|
|
183
|
+
parse_data[:parse_count] += 1
|
|
184
|
+
|
|
185
|
+
message = if parser.finished?
|
|
186
|
+
if invalid_host?(env) || request_smuggling?(env)
|
|
187
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
188
|
+
elsif parser.has_body?
|
|
189
|
+
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
190
|
+
|
|
191
|
+
if max_body_size && parser.content_length > max_body_size
|
|
192
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
193
|
+
elsif parser.chunked?
|
|
194
|
+
decoded_body, chunked_state = decode_chunked(body_buffer, max_body_size)
|
|
195
|
+
|
|
196
|
+
case chunked_state
|
|
197
|
+
when :complete
|
|
198
|
+
env.delete(HTTP_TRANSFER_ENCODING)
|
|
199
|
+
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
200
|
+
when :too_large
|
|
201
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
202
|
+
when :malformed
|
|
203
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
204
|
+
else
|
|
205
|
+
data.merge(env: env, parse_data: parse_data)
|
|
206
|
+
end
|
|
207
|
+
elsif parser.content_length > body_buffer.bytesize
|
|
208
|
+
data.merge(env: env, parse_data: parse_data)
|
|
209
|
+
else
|
|
210
|
+
data.merge(env: env, body: body_buffer, parse_data: parse_data, complete: true)
|
|
211
|
+
end
|
|
212
|
+
else
|
|
213
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true)
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
data.merge(env: env, parse_data: parse_data)
|
|
217
|
+
end
|
|
218
|
+
Ractor.make_shareable(message)
|
|
219
|
+
end
|
|
220
|
+
|
|
158
221
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
159
222
|
# @rbs @server_port: Integer
|
|
160
223
|
# @rbs @server_port_string: String
|
|
@@ -202,6 +265,16 @@ module Raptor
|
|
|
202
265
|
}.freeze
|
|
203
266
|
end
|
|
204
267
|
|
|
268
|
+
# Instance-level wrapper around {Http.parser_worker} that binds this
|
|
269
|
+
# handler's env template and body-size limit into the worker proc.
|
|
270
|
+
#
|
|
271
|
+
# @return [Proc]
|
|
272
|
+
#
|
|
273
|
+
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
274
|
+
def parser_worker
|
|
275
|
+
Http.parser_worker(@env_template, @max_body_size)
|
|
276
|
+
end
|
|
277
|
+
|
|
205
278
|
# Instance-level wrapper around {Http.socket_write} that applies the
|
|
206
279
|
# configured `write_timeout`.
|
|
207
280
|
#
|
|
@@ -252,72 +325,45 @@ module Raptor
|
|
|
252
325
|
#
|
|
253
326
|
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
254
327
|
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
255
|
-
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
256
|
-
|
|
257
328
|
begin
|
|
258
|
-
socket
|
|
329
|
+
buffer = read_into_thread_buffer(socket)
|
|
259
330
|
rescue IO::WaitReadable
|
|
260
|
-
reactor.add(
|
|
261
|
-
id: id,
|
|
262
|
-
socket: socket,
|
|
263
|
-
remote_addr: remote_addr,
|
|
264
|
-
url_scheme: url_scheme
|
|
265
|
-
)
|
|
331
|
+
reactor.add(id: id, socket: socket, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
266
332
|
return
|
|
267
333
|
rescue EOFError, IOError
|
|
268
334
|
socket.close rescue nil
|
|
269
335
|
return
|
|
270
336
|
end
|
|
271
337
|
|
|
272
|
-
|
|
273
|
-
buffer
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
|
|
277
|
-
parser.reset
|
|
278
|
-
env = @env_template.dup
|
|
279
|
-
nread = begin
|
|
280
|
-
parser.execute(env, buffer, 0)
|
|
338
|
+
env, parse_data, nread, parser = begin
|
|
339
|
+
parse_next_request(buffer)
|
|
281
340
|
rescue HttpParserError
|
|
282
341
|
reject_malformed(socket)
|
|
283
342
|
return
|
|
284
343
|
end
|
|
285
|
-
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
286
344
|
|
|
287
|
-
body = nil
|
|
288
345
|
if !parser.finished?
|
|
289
346
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
290
347
|
return
|
|
291
348
|
elsif Http1.invalid_host?(env) || Http1.request_smuggling?(env)
|
|
292
349
|
reject_malformed(socket)
|
|
293
350
|
return
|
|
294
|
-
elsif parser.has_body?
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
end
|
|
351
|
+
elsif parser.has_body? && @max_body_size && parser.content_length > @max_body_size
|
|
352
|
+
reject_oversized(socket)
|
|
353
|
+
return
|
|
354
|
+
end
|
|
299
355
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
reject_malformed(socket)
|
|
312
|
-
return
|
|
313
|
-
else
|
|
314
|
-
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
315
|
-
return
|
|
316
|
-
end
|
|
317
|
-
elsif parser.content_length > body.bytesize
|
|
318
|
-
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
319
|
-
return
|
|
320
|
-
end
|
|
356
|
+
body = extract_body(buffer, env, parser, nread, decode_chunked: true)
|
|
357
|
+
case body
|
|
358
|
+
when :incomplete
|
|
359
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
360
|
+
return
|
|
361
|
+
when :too_large
|
|
362
|
+
reject_oversized(socket)
|
|
363
|
+
return
|
|
364
|
+
when :malformed
|
|
365
|
+
reject_malformed(socket)
|
|
366
|
+
return
|
|
321
367
|
end
|
|
322
368
|
|
|
323
369
|
thread_pool << proc do
|
|
@@ -325,77 +371,8 @@ module Raptor
|
|
|
325
371
|
end
|
|
326
372
|
end
|
|
327
373
|
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
# The returned Proc processes raw socket data through the appropriate
|
|
331
|
-
# HTTP parser and returns either a complete request state (ready for
|
|
332
|
-
# app processing) or incomplete request state (needs more data).
|
|
333
|
-
#
|
|
334
|
-
# @return [Proc] a Ractor-safe proc that accepts a state hash and returns an updated state hash
|
|
335
|
-
#
|
|
336
|
-
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
337
|
-
def http_parser_worker
|
|
338
|
-
max_body_size = @max_body_size
|
|
339
|
-
env_template = @env_template
|
|
340
|
-
|
|
341
|
-
proc do |data|
|
|
342
|
-
next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
|
|
343
|
-
|
|
344
|
-
parser = Raptor::HttpParser.new
|
|
345
|
-
env = env_template.dup
|
|
346
|
-
nread = begin
|
|
347
|
-
parser.execute(env, data[:buffer], 0)
|
|
348
|
-
rescue Raptor::HttpParserError
|
|
349
|
-
next Ractor.make_shareable(data.merge(complete: true, malformed: true))
|
|
350
|
-
end
|
|
351
|
-
parse_data = if data[:parse_data]
|
|
352
|
-
data[:parse_data].dup
|
|
353
|
-
else
|
|
354
|
-
{ parse_count: 0, content_length: parser.content_length }
|
|
355
|
-
end
|
|
356
|
-
parse_data[:parse_count] += 1
|
|
357
|
-
|
|
358
|
-
message = if parser.finished?
|
|
359
|
-
if Raptor::Http1.invalid_host?(env) || Raptor::Http1.request_smuggling?(env)
|
|
360
|
-
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
361
|
-
elsif parser.has_body?
|
|
362
|
-
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
363
|
-
|
|
364
|
-
if max_body_size && parser.content_length > max_body_size
|
|
365
|
-
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
366
|
-
elsif parser.chunked?
|
|
367
|
-
decoded_body, chunked_state = Raptor::Http1.decode_chunked(body_buffer, max_body_size)
|
|
368
|
-
|
|
369
|
-
case chunked_state
|
|
370
|
-
when :complete
|
|
371
|
-
env.delete(HTTP_TRANSFER_ENCODING)
|
|
372
|
-
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
373
|
-
when :too_large
|
|
374
|
-
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
375
|
-
when :malformed
|
|
376
|
-
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
377
|
-
else
|
|
378
|
-
data.merge(env: env, parse_data: parse_data)
|
|
379
|
-
end
|
|
380
|
-
elsif parser.content_length > body_buffer.bytesize
|
|
381
|
-
data.merge(env: env, parse_data: parse_data)
|
|
382
|
-
else
|
|
383
|
-
data.merge(env: env, body: body_buffer, parse_data: parse_data, complete: true)
|
|
384
|
-
end
|
|
385
|
-
else
|
|
386
|
-
data.merge(env: env, body: nil, parse_data: parse_data, complete: true)
|
|
387
|
-
end
|
|
388
|
-
else
|
|
389
|
-
data.merge(env: env, parse_data: parse_data)
|
|
390
|
-
end
|
|
391
|
-
Ractor.make_shareable(message)
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# Handles a parsed HTTP request by either continuing parsing or dispatching to the Rack app.
|
|
396
|
-
#
|
|
397
|
-
# For incomplete requests, updates reactor state and re-registers for more I/O.
|
|
398
|
-
# For complete requests, removes from reactor, builds Rack env, and dispatches to thread pool.
|
|
374
|
+
# Dispatches a parsed HTTP request to the thread pool when complete,
|
|
375
|
+
# or hands it back to the reactor for more I/O when incomplete.
|
|
399
376
|
#
|
|
400
377
|
# @param parsed_request [Hash] the parsed request state from the ractor pool
|
|
401
378
|
# @param reactor [Reactor] the reactor managing the client connection
|
|
@@ -444,6 +421,79 @@ module Raptor
|
|
|
444
421
|
|
|
445
422
|
private
|
|
446
423
|
|
|
424
|
+
# Reads pending bytes off `socket` into the thread-local read buffer,
|
|
425
|
+
# draining any additional SSL-buffered bytes so `pending` is empty on
|
|
426
|
+
# return. Raises `IO::WaitReadable`, `EOFError`, or `IOError` like the
|
|
427
|
+
# underlying `read_nonblock` does.
|
|
428
|
+
#
|
|
429
|
+
# @param socket [TCPSocket] the socket to read from
|
|
430
|
+
# @return [String] the thread-local buffer, freshly populated
|
|
431
|
+
#
|
|
432
|
+
# @rbs (TCPSocket socket) -> String
|
|
433
|
+
def read_into_thread_buffer(socket)
|
|
434
|
+
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
435
|
+
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
436
|
+
|
|
437
|
+
while socket.respond_to?(:pending) && socket.pending > 0
|
|
438
|
+
buffer << socket.read_nonblock(socket.pending)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
buffer
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Runs a fresh HTTP/1.x parse against `buffer`, returning
|
|
445
|
+
# `[env, parse_data, nread, parser]`. Raises `HttpParserError` on
|
|
446
|
+
# malformed input.
|
|
447
|
+
#
|
|
448
|
+
# @param buffer [String] the raw request bytes
|
|
449
|
+
# @return [Array(Hash, Hash, Integer, HttpParser)]
|
|
450
|
+
#
|
|
451
|
+
# @rbs (String buffer) -> [Hash[String, untyped], Hash[Symbol, untyped], Integer, HttpParser]
|
|
452
|
+
def parse_next_request(buffer)
|
|
453
|
+
parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
|
|
454
|
+
parser.reset
|
|
455
|
+
env = @env_template.dup
|
|
456
|
+
nread = parser.execute(env, buffer, 0)
|
|
457
|
+
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
458
|
+
[env, parse_data, nread, parser]
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Resolves the request body for a finished parse. Returns the body
|
|
462
|
+
# `String` (or `nil` when the request has no body), or one of
|
|
463
|
+
# `:incomplete`, `:too_large`, `:malformed` when the caller must
|
|
464
|
+
# fall back or reject.
|
|
465
|
+
#
|
|
466
|
+
# @param buffer [String] the raw request bytes
|
|
467
|
+
# @param env [Hash] the Rack environment being built
|
|
468
|
+
# @param parser [HttpParser] the parser holding the finished parse state
|
|
469
|
+
# @param nread [Integer] the byte offset where the body begins in `buffer`
|
|
470
|
+
# @param decode_chunked [Boolean] whether to decode chunked bodies inline; when false chunked bodies signal `:incomplete`
|
|
471
|
+
# @return [String, nil, Symbol]
|
|
472
|
+
#
|
|
473
|
+
# @rbs (String buffer, Hash[String, untyped] env, HttpParser parser, Integer nread, decode_chunked: bool) -> (String | Symbol)?
|
|
474
|
+
def extract_body(buffer, env, parser, nread, decode_chunked:)
|
|
475
|
+
return nil unless parser.has_body?
|
|
476
|
+
|
|
477
|
+
body = buffer.byteslice(nread..-1) || ""
|
|
478
|
+
|
|
479
|
+
if parser.chunked?
|
|
480
|
+
return :incomplete unless decode_chunked
|
|
481
|
+
|
|
482
|
+
body, chunked_state = Http1.decode_chunked(body, @max_body_size)
|
|
483
|
+
case chunked_state
|
|
484
|
+
when :complete
|
|
485
|
+
env.delete(HTTP_TRANSFER_ENCODING)
|
|
486
|
+
body
|
|
487
|
+
else
|
|
488
|
+
chunked_state
|
|
489
|
+
end
|
|
490
|
+
elsif parser.content_length > body.bytesize
|
|
491
|
+
:incomplete
|
|
492
|
+
else
|
|
493
|
+
body
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
447
497
|
# Returns true if the request expects a 100 Continue response per
|
|
448
498
|
# RFC 7231 section 5.1.1.
|
|
449
499
|
#
|
|
@@ -455,11 +505,9 @@ module Raptor
|
|
|
455
505
|
(env[Rack::SERVER_PROTOCOL] == HTTP_11) && env[HTTP_EXPECT]&.casecmp?(EXPECT_100_CONTINUE)
|
|
456
506
|
end
|
|
457
507
|
|
|
458
|
-
# Sends an HTTP 100 Continue response when
|
|
459
|
-
# `Expect: 100-continue
|
|
460
|
-
#
|
|
461
|
-
# Returns the state hash with `:continued` set when the response has been
|
|
462
|
-
# written. A write failure is silently ignored.
|
|
508
|
+
# Sends an HTTP 100 Continue response when the client requested
|
|
509
|
+
# `Expect: 100-continue`, returning the state hash with `:continued`
|
|
510
|
+
# set once written. A write failure is silently ignored.
|
|
463
511
|
#
|
|
464
512
|
# @param state [Hash] the partially-parsed connection state
|
|
465
513
|
# @param reactor [Reactor] the reactor holding the connection's socket
|
|
@@ -500,9 +548,9 @@ module Raptor
|
|
|
500
548
|
eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme) if keep_alive
|
|
501
549
|
end
|
|
502
550
|
|
|
503
|
-
# Builds the Rack env, calls the
|
|
504
|
-
#
|
|
505
|
-
#
|
|
551
|
+
# Processes a single request. Builds the Rack env, calls the app,
|
|
552
|
+
# writes the response, and returns whether the connection stays open
|
|
553
|
+
# for another request.
|
|
506
554
|
#
|
|
507
555
|
# @param socket [TCPSocket] the client socket
|
|
508
556
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
@@ -542,15 +590,8 @@ module Raptor
|
|
|
542
590
|
call_response_finished(rack_env, status, headers, nil)
|
|
543
591
|
keep_alive && !hijacked
|
|
544
592
|
rescue => error
|
|
545
|
-
call_response_finished(rack_env, status, headers, error) if rack_env
|
|
546
|
-
socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
|
|
547
593
|
keep_alive = false
|
|
548
|
-
|
|
549
|
-
if @on_error
|
|
550
|
-
@on_error.call(rack_env, error) rescue nil
|
|
551
|
-
else
|
|
552
|
-
raise
|
|
553
|
-
end
|
|
594
|
+
handle_app_error(socket, rack_env, status, headers, error, response_started: response_started, hijacked: hijacked)
|
|
554
595
|
ensure
|
|
555
596
|
rack_input = rack_env && rack_env[Rack::RACK_INPUT]
|
|
556
597
|
rack_input.close! rescue nil if rack_input.respond_to?(:close!)
|
|
@@ -561,11 +602,35 @@ module Raptor
|
|
|
561
602
|
end
|
|
562
603
|
end
|
|
563
604
|
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#
|
|
605
|
+
# Handles an exception raised while processing a request. Fires the
|
|
606
|
+
# `rack.response_finished` callbacks with the error, writes a 500
|
|
607
|
+
# response when no bytes have gone to the socket yet, and routes the
|
|
608
|
+
# exception through the configured `on_error` handler (or re-raises).
|
|
609
|
+
#
|
|
610
|
+
# @param socket [TCPSocket] the client socket
|
|
611
|
+
# @param rack_env [Hash, nil] the Rack environment, if it was built
|
|
612
|
+
# @param status [Integer, nil] the status returned by the app, if any
|
|
613
|
+
# @param headers [Hash, nil] the headers returned by the app, if any
|
|
614
|
+
# @param error [Exception] the exception raised
|
|
615
|
+
# @param response_started [Boolean] whether any response bytes have been written
|
|
616
|
+
# @param hijacked [Boolean] whether the app took over the socket
|
|
617
|
+
# @return [void]
|
|
618
|
+
#
|
|
619
|
+
# @rbs (TCPSocket socket, Hash[String, untyped]? rack_env, Integer? status, Hash[String, String | Array[String]]? headers, Exception error, response_started: bool, hijacked: bool) -> void
|
|
620
|
+
def handle_app_error(socket, rack_env, status, headers, error, response_started:, hijacked:)
|
|
621
|
+
call_response_finished(rack_env, status, headers, error) if rack_env
|
|
622
|
+
socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
|
|
623
|
+
|
|
624
|
+
if @on_error
|
|
625
|
+
@on_error.call(rack_env, error) rescue nil
|
|
626
|
+
else
|
|
627
|
+
raise error
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Reads and processes subsequent requests inline on a kept-alive
|
|
632
|
+
# connection. Falls back to the reactor when no data arrives within the
|
|
633
|
+
# timeout, the thread pool is saturated, or the request is incomplete.
|
|
569
634
|
#
|
|
570
635
|
# @param socket [TCPSocket] the client socket
|
|
571
636
|
# @param id [Integer] unique client identifier
|
|
@@ -589,10 +654,8 @@ module Raptor
|
|
|
589
654
|
return
|
|
590
655
|
end
|
|
591
656
|
|
|
592
|
-
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
593
|
-
|
|
594
657
|
begin
|
|
595
|
-
socket
|
|
658
|
+
buffer = read_into_thread_buffer(socket)
|
|
596
659
|
rescue IO::WaitReadable
|
|
597
660
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
598
661
|
return
|
|
@@ -601,32 +664,22 @@ module Raptor
|
|
|
601
664
|
return
|
|
602
665
|
end
|
|
603
666
|
|
|
604
|
-
|
|
605
|
-
buffer
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
|
|
609
|
-
parser.reset
|
|
610
|
-
env = @env_template.dup
|
|
611
|
-
nread = begin
|
|
612
|
-
parser.execute(env, buffer, 0)
|
|
667
|
+
env, parse_data, nread, parser = begin
|
|
668
|
+
parse_next_request(buffer)
|
|
613
669
|
rescue HttpParserError
|
|
614
670
|
reject_malformed(socket)
|
|
615
671
|
return
|
|
616
672
|
end
|
|
617
|
-
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
618
673
|
|
|
619
|
-
body = nil
|
|
620
674
|
if !parser.finished?
|
|
621
675
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
622
676
|
return
|
|
623
|
-
|
|
624
|
-
body = buffer.byteslice(nread..-1) || ""
|
|
677
|
+
end
|
|
625
678
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
679
|
+
body = extract_body(buffer, env, parser, nread, decode_chunked: false)
|
|
680
|
+
if body == :incomplete
|
|
681
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
682
|
+
return
|
|
630
683
|
end
|
|
631
684
|
|
|
632
685
|
request_count += 1
|
|
@@ -663,11 +716,7 @@ module Raptor
|
|
|
663
716
|
end
|
|
664
717
|
|
|
665
718
|
# Re-registers a socket with the reactor for further processing when
|
|
666
|
-
# an incomplete request is received
|
|
667
|
-
#
|
|
668
|
-
# The persisted flag selects between persistent_data_timeout (for
|
|
669
|
-
# kept-alive connections awaiting the next request) and chunk_data_timeout
|
|
670
|
-
# (for fresh connections awaiting the rest of the first request).
|
|
719
|
+
# an incomplete request is received on the fast path.
|
|
671
720
|
#
|
|
672
721
|
# @param socket [TCPSocket] the client socket
|
|
673
722
|
# @param id [Integer] unique client identifier
|
|
@@ -701,8 +750,7 @@ module Raptor
|
|
|
701
750
|
reactor.update_state(Ractor.make_shareable(state))
|
|
702
751
|
end
|
|
703
752
|
|
|
704
|
-
# Writes a 413 response and closes the socket.
|
|
705
|
-
# exceeds the configured maximum size.
|
|
753
|
+
# Writes a 413 response and closes the socket.
|
|
706
754
|
#
|
|
707
755
|
# @param socket [TCPSocket] the client socket
|
|
708
756
|
# @return [void]
|
|
@@ -713,8 +761,7 @@ module Raptor
|
|
|
713
761
|
socket.close rescue nil
|
|
714
762
|
end
|
|
715
763
|
|
|
716
|
-
# Writes a 400 response and closes the socket.
|
|
717
|
-
# rejects the request line or headers.
|
|
764
|
+
# Writes a 400 response and closes the socket.
|
|
718
765
|
#
|
|
719
766
|
# @param socket [TCPSocket] the client socket
|
|
720
767
|
# @return [void]
|
|
@@ -727,9 +774,6 @@ module Raptor
|
|
|
727
774
|
|
|
728
775
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
729
776
|
#
|
|
730
|
-
# Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
|
|
731
|
-
# SERVER_NAME, SERVER_PORT, and hijack support.
|
|
732
|
-
#
|
|
733
777
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
734
778
|
# @param parse_data [Hash] metadata from the parsing pass, including content_length
|
|
735
779
|
# @param body [String, nil] decoded request body, or nil if no body
|
|
@@ -842,11 +886,8 @@ module Raptor
|
|
|
842
886
|
env["HTTP_X_FORWARDED_SSL"]&.casecmp?("on") || false
|
|
843
887
|
end
|
|
844
888
|
|
|
845
|
-
#
|
|
846
|
-
#
|
|
847
|
-
# Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
|
|
848
|
-
# is the default unless the client sent Connection: close. For HTTP/1.0,
|
|
849
|
-
# keep-alive must be explicitly requested.
|
|
889
|
+
# Returns true when the connection should be kept alive after the
|
|
890
|
+
# current response.
|
|
850
891
|
#
|
|
851
892
|
# @param env [Hash] the Rack environment
|
|
852
893
|
# @param request_count [Integer] number of requests handled on this connection
|
|
@@ -865,9 +906,8 @@ module Raptor
|
|
|
865
906
|
end
|
|
866
907
|
end
|
|
867
908
|
|
|
868
|
-
# Sends an HTTP 103 Early Hints response
|
|
869
|
-
#
|
|
870
|
-
# Skips any hints with illegal header keys or values. No-ops if hints is empty.
|
|
909
|
+
# Sends an HTTP 103 Early Hints response, skipping any entries with
|
|
910
|
+
# illegal header keys or values. No-ops when `hints` is empty.
|
|
871
911
|
#
|
|
872
912
|
# @param socket [TCPSocket] the client socket to write to
|
|
873
913
|
# @param hints [Hash] header name to value (or array of values) pairs
|
|
@@ -895,9 +935,6 @@ module Raptor
|
|
|
895
935
|
|
|
896
936
|
# Writes a complete HTTP response to the socket.
|
|
897
937
|
#
|
|
898
|
-
# Handles header normalization, validation, connection management, TCP corking,
|
|
899
|
-
# and dispatches to the appropriate body write strategy.
|
|
900
|
-
#
|
|
901
938
|
# @param socket [TCPSocket] the client socket to write to
|
|
902
939
|
# @param env [Hash] the Rack environment
|
|
903
940
|
# @param status [Integer] HTTP status code
|
|
@@ -949,10 +986,8 @@ module Raptor
|
|
|
949
986
|
raise ArgumentError, "status must be >= 100" unless status >= 100
|
|
950
987
|
end
|
|
951
988
|
|
|
952
|
-
#
|
|
953
|
-
#
|
|
954
|
-
# Removes headers with illegal keys, rack.* prefixed headers, and "status" headers.
|
|
955
|
-
# Raises if headers is not a Hash or contains non-String keys.
|
|
989
|
+
# Returns a normalised copy of the response headers with lowercased
|
|
990
|
+
# keys and illegal/`rack.*`/`status` entries dropped.
|
|
956
991
|
#
|
|
957
992
|
# @param headers [Hash] raw headers from the Rack application
|
|
958
993
|
# @return [Hash] normalized headers with lowercased string keys
|
|
@@ -977,10 +1012,8 @@ module Raptor
|
|
|
977
1012
|
normalized
|
|
978
1013
|
end
|
|
979
1014
|
|
|
980
|
-
#
|
|
981
|
-
#
|
|
982
|
-
# Raises if content-type or content-length are present for status codes
|
|
983
|
-
# that must not have an entity body (204, 304, 1xx).
|
|
1015
|
+
# Raises when the headers include entries forbidden for the response
|
|
1016
|
+
# status (`content-type` or `content-length` on a 204, 304, or 1xx).
|
|
984
1017
|
#
|
|
985
1018
|
# @param headers [Hash] normalized response headers
|
|
986
1019
|
# @param status [Integer] HTTP status code
|
|
@@ -996,8 +1029,7 @@ module Raptor
|
|
|
996
1029
|
end
|
|
997
1030
|
end
|
|
998
1031
|
|
|
999
|
-
# Returns the HTTP status line for `status`.
|
|
1000
|
-
# returned string across further calls on the same thread.
|
|
1032
|
+
# Returns the HTTP status line for `status`.
|
|
1001
1033
|
#
|
|
1002
1034
|
# @param http_version [String] "HTTP/1.1" or "HTTP/1.0"
|
|
1003
1035
|
# @param status [Integer] HTTP status code
|
|
@@ -1012,10 +1044,8 @@ module Raptor
|
|
|
1012
1044
|
response
|
|
1013
1045
|
end
|
|
1014
1046
|
|
|
1015
|
-
# Writes response headers
|
|
1016
|
-
#
|
|
1017
|
-
# Uncorks the socket before calling the hijack so the app has full control
|
|
1018
|
-
# of the raw connection.
|
|
1047
|
+
# Writes the response headers, uncorks the socket, and hands the raw
|
|
1048
|
+
# socket to the hijack callback.
|
|
1019
1049
|
#
|
|
1020
1050
|
# @param socket [TCPSocket] the client socket
|
|
1021
1051
|
# @param response [String] the status line accumulated so far
|
|
@@ -1032,11 +1062,9 @@ module Raptor
|
|
|
1032
1062
|
response_hijack.call(socket)
|
|
1033
1063
|
end
|
|
1034
1064
|
|
|
1035
|
-
# Writes a response with no entity body
|
|
1036
|
-
#
|
|
1037
|
-
#
|
|
1038
|
-
# (204, 304, 1xx). Adds a zero content-length for non-no-body statuses
|
|
1039
|
-
# that did not supply one.
|
|
1065
|
+
# Writes a response with no entity body, adding a zero
|
|
1066
|
+
# `Content-Length` when the status may carry a body but none was
|
|
1067
|
+
# supplied.
|
|
1040
1068
|
#
|
|
1041
1069
|
# @param socket [TCPSocket] the client socket
|
|
1042
1070
|
# @param response [String] the status line accumulated so far
|
|
@@ -1055,12 +1083,9 @@ module Raptor
|
|
|
1055
1083
|
socket_write(socket, response)
|
|
1056
1084
|
end
|
|
1057
1085
|
|
|
1058
|
-
# Writes a complete response with a body.
|
|
1059
|
-
#
|
|
1060
|
-
#
|
|
1061
|
-
# file (zero-copy), array, or generic enumerable. Automatically determines
|
|
1062
|
-
# content-length where possible, falling back to chunked transfer encoding
|
|
1063
|
-
# for HTTP/1.1 when the length cannot be determined upfront.
|
|
1086
|
+
# Writes a complete response with a body. Emits a `Content-Length`
|
|
1087
|
+
# when the total size is known upfront, otherwise chunked encoding on
|
|
1088
|
+
# HTTP/1.1.
|
|
1064
1089
|
#
|
|
1065
1090
|
# @param socket [TCPSocket] the client socket
|
|
1066
1091
|
# @param response [String] the status line accumulated so far
|
|
@@ -1112,9 +1137,8 @@ module Raptor
|
|
|
1112
1137
|
end
|
|
1113
1138
|
end
|
|
1114
1139
|
|
|
1115
|
-
#
|
|
1116
|
-
#
|
|
1117
|
-
# Returns nil for enumerable bodies whose length cannot be determined upfront.
|
|
1140
|
+
# Returns the byte length of the body when it can be determined
|
|
1141
|
+
# upfront (array or file), otherwise nil.
|
|
1118
1142
|
#
|
|
1119
1143
|
# @param body [Object] the response body
|
|
1120
1144
|
# @return [Integer, nil] the byte length, or nil if it cannot be determined
|
|
@@ -1135,9 +1159,6 @@ module Raptor
|
|
|
1135
1159
|
|
|
1136
1160
|
# Writes a file body to the socket.
|
|
1137
1161
|
#
|
|
1138
|
-
# Uses zero-copy IO.copy_stream for large files, direct buffering for small ones,
|
|
1139
|
-
# and chunked encoding when required.
|
|
1140
|
-
#
|
|
1141
1162
|
# @param socket [TCPSocket] the client socket
|
|
1142
1163
|
# @param response [String] headers already serialized, to be written before the body
|
|
1143
1164
|
# @param path [String] filesystem path of the file to send
|
|
@@ -1171,8 +1192,6 @@ module Raptor
|
|
|
1171
1192
|
|
|
1172
1193
|
# Writes an array body to the socket.
|
|
1173
1194
|
#
|
|
1174
|
-
# Dispatches to the single-chunk or multi-chunk path based on array length.
|
|
1175
|
-
#
|
|
1176
1195
|
# @param socket [TCPSocket] the client socket
|
|
1177
1196
|
# @param response [String] headers already serialized, to be written before the body
|
|
1178
1197
|
# @param body_array [Array<String>] the response body chunks
|
|
@@ -1294,10 +1313,8 @@ module Raptor
|
|
|
1294
1313
|
value.match?(ILLEGAL_HEADER_VALUE_REGEX)
|
|
1295
1314
|
end
|
|
1296
1315
|
|
|
1297
|
-
#
|
|
1298
|
-
#
|
|
1299
|
-
# Skips entries with illegal keys or values. Array values are written
|
|
1300
|
-
# as separate header lines.
|
|
1316
|
+
# Appends normalised header lines to `result`. Skips entries with
|
|
1317
|
+
# illegal keys or values. Array values are written as separate lines.
|
|
1301
1318
|
#
|
|
1302
1319
|
# @param headers [Hash] normalized response headers
|
|
1303
1320
|
# @return [String] formatted header lines, each ending with CRLF
|
|
@@ -1315,9 +1332,9 @@ module Raptor
|
|
|
1315
1332
|
end
|
|
1316
1333
|
end
|
|
1317
1334
|
|
|
1318
|
-
# Appends one or more `name: value` header lines to `result
|
|
1319
|
-
#
|
|
1320
|
-
#
|
|
1335
|
+
# Appends one or more `name: value` header lines to `result`, splitting
|
|
1336
|
+
# newline-joined values across separate lines and skipping empty or
|
|
1337
|
+
# illegal values.
|
|
1321
1338
|
#
|
|
1322
1339
|
# @param result [String] the buffer to append to
|
|
1323
1340
|
# @param name [String] the header name
|
|
@@ -1342,10 +1359,8 @@ module Raptor
|
|
|
1342
1359
|
end
|
|
1343
1360
|
end
|
|
1344
1361
|
|
|
1345
|
-
# Calls
|
|
1346
|
-
#
|
|
1347
|
-
# Callbacks are called in reverse registration order. Individual callback
|
|
1348
|
-
# failures are rescued so all callbacks are always attempted.
|
|
1362
|
+
# Calls every `rack.response_finished` callback in reverse
|
|
1363
|
+
# registration order, rescuing any that raise.
|
|
1349
1364
|
#
|
|
1350
1365
|
# @param env [Hash, nil] the Rack environment
|
|
1351
1366
|
# @param status [Integer, nil] the response status code
|
|
@@ -1390,10 +1405,8 @@ module Raptor
|
|
|
1390
1405
|
end
|
|
1391
1406
|
|
|
1392
1407
|
if Socket.const_defined?(:TCP_CORK)
|
|
1393
|
-
# Enables TCP_CORK on the socket to batch outgoing packets into
|
|
1394
|
-
#
|
|
1395
|
-
# Only applies to TCP sockets. No-op on non-TCP sockets.
|
|
1396
|
-
# Available on Linux only; this method is not defined on other platforms.
|
|
1408
|
+
# Enables `TCP_CORK` on the socket to batch outgoing packets into
|
|
1409
|
+
# fewer segments. Linux-only; a no-op elsewhere.
|
|
1397
1410
|
#
|
|
1398
1411
|
# @param socket [TCPSocket] the socket to cork
|
|
1399
1412
|
# @return [void]
|
|
@@ -1403,10 +1416,8 @@ module Raptor
|
|
|
1403
1416
|
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if socket.is_a?(TCPSocket)
|
|
1404
1417
|
end
|
|
1405
1418
|
|
|
1406
|
-
# Disables TCP_CORK on the socket, flushing any buffered packets.
|
|
1407
|
-
#
|
|
1408
|
-
# Only applies to TCP sockets. No-op on non-TCP sockets.
|
|
1409
|
-
# Available on Linux only; this method is not defined on other platforms.
|
|
1419
|
+
# Disables `TCP_CORK` on the socket, flushing any buffered packets.
|
|
1420
|
+
# Linux-only; a no-op elsewhere.
|
|
1410
1421
|
#
|
|
1411
1422
|
# @param socket [TCPSocket] the socket to uncork
|
|
1412
1423
|
# @return [void]
|