raptor 0.1.0 → 0.3.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.
data/lib/raptor/http2.rb CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "stringio"
5
5
 
6
+ require "atomic-ruby/atom"
6
7
  require "rack"
7
8
 
8
9
  require_relative "raptor_http2"
@@ -16,6 +17,63 @@ module Raptor
16
17
  # pipeline used by HTTP/1.1 connections.
17
18
  #
18
19
  class Http2
20
+ # Lock-free per-connection frame writer.
21
+ #
22
+ # Serializes concurrent socket writes from multiple stream workers
23
+ # without blocking any of them.
24
+ #
25
+ class Writer
26
+ IDLE = :idle
27
+
28
+ # @rbs @state: Atom
29
+
30
+ # Creates a new Writer.
31
+ #
32
+ # @rbs () -> void
33
+ def initialize
34
+ @state = Atom.new(IDLE)
35
+ end
36
+
37
+ # Writes frames to the socket, coordinating with concurrent writers
38
+ # so that exactly one thread is actively writing at any time.
39
+ #
40
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
41
+ # @param frames [Array<String>] frame bytes to write in order
42
+ # @return [void]
43
+ #
44
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
45
+ def write_frames(socket, frames)
46
+ return if frames.nil? || frames.empty?
47
+
48
+ claimed = false
49
+ @state.swap do |current|
50
+ if current.equal?(IDLE)
51
+ claimed = true
52
+ frames
53
+ else
54
+ claimed = false
55
+ current + frames
56
+ end
57
+ end
58
+
59
+ return unless claimed
60
+
61
+ loop do
62
+ pending = nil
63
+ @state.swap do |current|
64
+ pending = current
65
+ current.empty? ? IDLE : []
66
+ end
67
+
68
+ break if pending.empty?
69
+
70
+ pending.each do |frame|
71
+ socket.write(frame) rescue nil
72
+ end
73
+ end
74
+ end
75
+ end
76
+
19
77
  FLAG_END_STREAM = 0x1
20
78
  FLAG_END_HEADERS = 0x4
21
79
  FLAG_ACK = 0x1
@@ -27,17 +85,20 @@ module Raptor
27
85
 
28
86
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
29
87
  # @rbs @server_port: Integer
88
+ # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
30
89
 
31
90
  # Creates a new Http2 handler.
32
91
  #
33
92
  # @param app [#call] the Rack application to dispatch requests to
34
93
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
94
+ # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
35
95
  # @return [void]
36
96
  #
37
- # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
38
- def initialize(app, server_port)
97
+ # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
98
+ def initialize(app, server_port, on_error: nil)
39
99
  @app = app
40
100
  @server_port = server_port
101
+ @on_error = on_error
41
102
  end
42
103
 
43
104
  # Builds the initial server SETTINGS frame to send on connection establishment.
@@ -215,13 +276,9 @@ module Raptor
215
276
  socket = reactor.socket_for(result[:id])
216
277
  return unless socket
217
278
 
218
- mutex = reactor.mutex_for(result[:id])
279
+ writer = reactor.writer_for(result[:id])
219
280
 
220
- if result[:outgoing_frames]&.any?
221
- mutex.synchronize do
222
- result[:outgoing_frames].each { |frame| socket.write(frame) rescue nil }
223
- end
224
- end
281
+ writer.write_frames(socket, result[:outgoing_frames])
225
282
 
226
283
  reactor.update_http2_state(result)
227
284
 
@@ -231,7 +288,7 @@ module Raptor
231
288
 
232
289
  thread_pool << proc do
233
290
  dispatch_stream_request(
234
- socket, mutex, stream_id,
291
+ socket, writer, stream_id,
235
292
  request[:headers], request[:body],
236
293
  remote_addr: remote_addr
237
294
  )
@@ -245,22 +302,27 @@ module Raptor
245
302
  # the response back as HTTP/2 frames.
246
303
  #
247
304
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
248
- # @param mutex [Mutex] mutex for serializing writes to the connection socket
305
+ # @param writer [Writer] lock-free frame writer for the connection
249
306
  # @param stream_id [Integer] the HTTP/2 stream identifier
250
307
  # @param headers [Array<Array(String, String)>] request headers
251
308
  # @param body [String] request body
252
309
  # @param remote_addr [String] the client IP address
253
310
  # @return [void]
254
311
  #
255
- # @rbs (OpenSSL::SSL::SSLSocket socket, Mutex mutex, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
256
- def dispatch_stream_request(socket, mutex, stream_id, headers, body, remote_addr:)
312
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
313
+ def dispatch_stream_request(socket, writer, stream_id, headers, body, remote_addr:)
257
314
  env = build_rack_env(headers, body, remote_addr: remote_addr)
258
315
  status, response_headers, response_body = @app.call(env)
259
316
 
260
- write_http2_response(socket, mutex, stream_id, status, response_headers, response_body)
261
- rescue
262
- write_http2_error_response(socket, mutex, stream_id)
263
- raise
317
+ write_http2_response(socket, writer, stream_id, status, response_headers, response_body)
318
+ rescue => error
319
+ write_http2_error_response(socket, writer, stream_id)
320
+
321
+ if @on_error
322
+ @on_error.call(env, error) rescue nil
323
+ else
324
+ raise
325
+ end
264
326
  ensure
265
327
  response_body.close if response_body.respond_to?(:close)
266
328
  end
@@ -268,15 +330,15 @@ module Raptor
268
330
  # Writes a Rack response as HTTP/2 frames to the socket.
269
331
  #
270
332
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
271
- # @param mutex [Mutex] mutex for serializing writes to the connection socket
333
+ # @param writer [Writer] lock-free frame writer for the connection
272
334
  # @param stream_id [Integer] the HTTP/2 stream identifier
273
335
  # @param status [Integer] HTTP status code
274
336
  # @param headers [Hash] response headers from the Rack application
275
337
  # @param body [Object] response body responding to each
276
338
  # @return [void]
277
339
  #
278
- # @rbs (OpenSSL::SSL::SSLSocket socket, Mutex mutex, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
279
- def write_http2_response(socket, mutex, stream_id, status, headers, body)
340
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
341
+ def write_http2_response(socket, writer, stream_id, status, headers, body)
280
342
  parser = Http2Parser.new
281
343
 
282
344
  header_pairs = [[":status", status.to_s]]
@@ -296,40 +358,38 @@ module Raptor
296
358
  body_chunks = []
297
359
  body.each { |chunk| body_chunks << chunk unless chunk.empty? }
298
360
 
299
- mutex.synchronize do
300
- if body_chunks.empty?
301
- socket.write(parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers))
302
- else
303
- socket.write(parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers))
361
+ frames = []
362
+ if body_chunks.empty?
363
+ frames << parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)
364
+ else
365
+ frames << parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)
304
366
 
305
- body_chunks.each_with_index do |chunk, index|
306
- last = index == body_chunks.size - 1
307
- flags = last ? FLAG_END_STREAM : 0
308
- socket.write(parser.build_frame(:data, flags, stream_id, chunk))
309
- end
367
+ last_index = body_chunks.size - 1
368
+ body_chunks.each_with_index do |chunk, index|
369
+ flags = index == last_index ? FLAG_END_STREAM : 0
370
+ frames << parser.build_frame(:data, flags, stream_id, chunk)
310
371
  end
311
372
  end
312
- rescue IOError, Errno::EPIPE
313
- # Connection closed
373
+
374
+ writer.write_frames(socket, frames)
314
375
  end
315
376
 
316
377
  # Writes a 500 error response as HTTP/2 frames.
317
378
  #
318
379
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
319
- # @param mutex [Mutex] mutex for serializing writes to the connection socket
380
+ # @param writer [Writer] lock-free frame writer for the connection
320
381
  # @param stream_id [Integer] the HTTP/2 stream identifier
321
382
  # @return [void]
322
383
  #
323
- # @rbs (OpenSSL::SSL::SSLSocket socket, Mutex mutex, Integer stream_id) -> void
324
- def write_http2_error_response(socket, mutex, stream_id)
384
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
385
+ def write_http2_error_response(socket, writer, stream_id)
325
386
  parser = Http2Parser.new
326
387
  encoded = parser.encode_headers([[":status", "500"]])
327
388
 
328
- mutex.synchronize do
329
- socket.write(parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded))
330
- end
331
- rescue IOError, Errno::EPIPE
332
- # Connection closed
389
+ writer.write_frames(
390
+ socket,
391
+ [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded)]
392
+ )
333
393
  end
334
394
 
335
395
  # Builds a Rack environment hash from HTTP/2 headers and body.
@@ -75,7 +75,7 @@ module Raptor
75
75
  # @rbs @id_to_socket: Hash[Integer, TCPSocket]
76
76
  # @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
77
77
  # @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
78
- # @rbs @id_to_mutex: Hash[Integer, Mutex]
78
+ # @rbs @id_to_writer: Hash[Integer, untyped]
79
79
 
80
80
  # Creates a new Reactor instance.
81
81
  #
@@ -100,7 +100,7 @@ module Raptor
100
100
  @id_to_socket = {}
101
101
  @socket_to_state = {}
102
102
  @id_to_timeout = {}
103
- @id_to_mutex = {}
103
+ @id_to_writer = {}
104
104
  end
105
105
 
106
106
  # Starts the reactor's main event loop in a new thread.
@@ -162,12 +162,10 @@ module Raptor
162
162
  def add(state)
163
163
  socket = state[:socket]
164
164
  state.delete(:socket)
165
+ writer = state.delete(:writer)
165
166
  @id_to_socket[state[:id]] = socket
166
167
  @socket_to_state[socket] = state
167
-
168
- if state[:protocol] == :http2
169
- @id_to_mutex[state[:id]] = Mutex.new
170
- end
168
+ @id_to_writer[state[:id]] = writer if writer
171
169
 
172
170
  read_and_queue_for_parse(socket, state)
173
171
  end
@@ -252,14 +250,16 @@ module Raptor
252
250
  @id_to_socket[id]
253
251
  end
254
252
 
255
- # Returns the mutex for a given HTTP/2 connection.
253
+ # Returns the writer object associated with a given connection, if one
254
+ # was supplied when the connection was added. Used by protocol handlers
255
+ # that need to coordinate concurrent socket writes.
256
256
  #
257
257
  # @param id [Integer] unique client identifier
258
- # @return [Mutex, nil] the mutex, if found
258
+ # @return [Object, nil] the writer, if found
259
259
  #
260
- # @rbs (Integer id) -> Mutex?
261
- def mutex_for(id)
262
- @id_to_mutex[id]
260
+ # @rbs (Integer id) -> untyped?
261
+ def writer_for(id)
262
+ @id_to_writer[id]
263
263
  end
264
264
 
265
265
  # Updates connection state for an HTTP/2 connection after frame processing.
@@ -384,7 +384,7 @@ module Raptor
384
384
  def cleanup(socket)
385
385
  state = @socket_to_state.delete(socket)
386
386
  @id_to_socket.delete(state[:id])
387
- @id_to_mutex.delete(state[:id])
387
+ @id_to_writer.delete(state[:id])
388
388
  socket.close
389
389
  end
390
390
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "socket"
5
5
  require "stringio"
6
+ require "tempfile"
6
7
 
7
8
  require "rack"
8
9
 
@@ -20,7 +21,7 @@ module Raptor
20
21
  class Request
21
22
  BODY_BUFFER_THRESHOLD = 256 * 1024
22
23
  FILE_CHUNK_SIZE = 64 * 1024
23
- KEEPALIVE_BUFFER_SIZE = 64 * 1024
24
+ READ_BUFFER_SIZE = 64 * 1024
24
25
  WRITE_TIMEOUT = 5
25
26
  KEEPALIVE_READ_TIMEOUT = 0.001
26
27
  MAX_KEEPALIVE_REQUESTS = 100
@@ -38,7 +39,8 @@ module Raptor
38
39
  end
39
40
 
40
41
  STATUS_WITH_NO_ENTITY_BODY = Set.new([204, 304, *100..199]).freeze
41
- ERROR_RESPONSE_500 = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
42
+ INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
43
+ CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
42
44
 
43
45
  CONNECTION_CLOSE = "close"
44
46
  CONNECTION_KEEPALIVE = "keep-alive"
@@ -59,19 +61,136 @@ module Raptor
59
61
  def message = "could not write response"
60
62
  end
61
63
 
64
+ # Decodes a chunked transfer-encoded body buffer.
65
+ #
66
+ # Returns the decoded bytes and a state symbol: `:complete` when the
67
+ # terminating zero-length chunk was found, `:too_large` when the decoded
68
+ # size would exceed `max_size`, or `:incomplete` otherwise.
69
+ #
70
+ # @param buffer [String] the raw body buffer to decode
71
+ # @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
72
+ # @return [Array(String, Symbol)] decoded body and completion state
73
+ #
74
+ # @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
75
+ def self.decode_chunked(buffer, max_size = nil)
76
+ decoded = String.new
77
+ offset = 0
78
+
79
+ while offset < buffer.bytesize
80
+ crlf = buffer.index("\r\n", offset)
81
+ return [decoded, :incomplete] unless crlf
82
+
83
+ chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
84
+ return [decoded, :complete] if chunk_size == 0
85
+ return [decoded, :too_large] if max_size && decoded.bytesize + chunk_size > max_size
86
+
87
+ offset = crlf + 2
88
+ decoded << buffer.byteslice(offset, chunk_size)
89
+ offset += chunk_size + 2
90
+ end
91
+
92
+ [decoded, :incomplete]
93
+ end
94
+
62
95
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
63
96
  # @rbs @server_port: Integer
97
+ # @rbs @max_body_size: Integer?
98
+ # @rbs @body_spool_threshold: Integer?
99
+ # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
64
100
 
65
101
  # Creates a new Request handler.
66
102
  #
67
103
  # @param app [#call] the Rack application to dispatch complete requests to
68
104
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
105
+ # @param client_options [Hash] client limits configuration
106
+ # @option client_options [Integer, nil] :max_body_size maximum request body size in bytes
107
+ # @option client_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
108
+ # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
69
109
  # @return [void]
70
110
  #
71
- # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
72
- def initialize(app, server_port)
111
+ # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?client_options: Hash[Symbol, untyped], ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
112
+ def initialize(app, server_port, client_options: {}, on_error: nil)
73
113
  @app = app
74
114
  @server_port = server_port
115
+ @max_body_size = client_options[:max_body_size]
116
+ @body_spool_threshold = client_options[:body_spool_threshold]
117
+ @on_error = on_error
118
+ end
119
+
120
+ # Eagerly reads and parses the first request on a freshly accepted
121
+ # connection on the server thread, dispatching directly to the thread pool
122
+ # when complete. Falls back to the reactor when more data is needed.
123
+ #
124
+ # @param socket [TCPSocket] the freshly accepted client socket
125
+ # @param id [Integer] unique client identifier
126
+ # @param reactor [Reactor] the reactor for fallback registration
127
+ # @param thread_pool [AtomicThreadPool] thread pool for application processing
128
+ # @param remote_addr [String] client IP address
129
+ # @param url_scheme [String] "http" or "https"
130
+ # @return [void]
131
+ #
132
+ # @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
133
+ def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
134
+ data = begin
135
+ socket.read_nonblock(READ_BUFFER_SIZE)
136
+ rescue IO::WaitReadable
137
+ reactor.add(
138
+ id: id,
139
+ socket: socket,
140
+ remote_addr: remote_addr,
141
+ url_scheme: url_scheme
142
+ )
143
+ return
144
+ rescue EOFError, IOError
145
+ socket.close rescue nil
146
+ return
147
+ end
148
+
149
+ buffer = String.new
150
+ buffer << data
151
+
152
+ while socket.respond_to?(:pending) && socket.pending > 0
153
+ buffer << socket.read_nonblock(socket.pending)
154
+ end
155
+
156
+ parser = HttpParser.new
157
+ env = {}
158
+ nread = parser.execute(env, buffer, 0)
159
+ parse_data = { parse_count: 1, content_length: parser.content_length }
160
+
161
+ body = nil
162
+ if !parser.finished?
163
+ fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
164
+ return
165
+ elsif parser.has_body?
166
+ if @max_body_size && parser.content_length > @max_body_size
167
+ reject_oversized(socket)
168
+ return
169
+ end
170
+
171
+ body = buffer.byteslice(nread..-1) || ""
172
+
173
+ if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
174
+ body, chunked_state = Request.decode_chunked(body, @max_body_size)
175
+ case chunked_state
176
+ when :complete
177
+ env.delete(HTTP_TRANSFER_ENCODING)
178
+ when :too_large
179
+ reject_oversized(socket)
180
+ return
181
+ else
182
+ fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
183
+ return
184
+ end
185
+ elsif parser.content_length > body.bytesize
186
+ fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
187
+ return
188
+ end
189
+ end
190
+
191
+ thread_pool << proc do
192
+ process_client(socket, id, env, parse_data, body, reactor, thread_pool, 1, remote_addr, url_scheme)
193
+ end
75
194
  end
76
195
 
77
196
  # Returns a Proc for HTTP parsing work in Ractor context.
@@ -84,6 +203,8 @@ module Raptor
84
203
  #
85
204
  # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
86
205
  def http_parser_worker
206
+ max_body_size = @max_body_size
207
+
87
208
  proc do |data|
88
209
  next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
89
210
 
@@ -101,30 +222,17 @@ module Raptor
101
222
  if parser.has_body?
102
223
  body_buffer = data[:buffer].byteslice(nread..-1) || ""
103
224
 
104
- if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
105
- decoded_body = String.new
106
- offset = 0
107
- chunked_complete = false
225
+ if max_body_size && parser.content_length > max_body_size
226
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
227
+ elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
228
+ decoded_body, chunked_state = Raptor::Request.decode_chunked(body_buffer, max_body_size)
108
229
 
109
- while offset < body_buffer.bytesize
110
- crlf = body_buffer.index("\r\n", offset)
111
- break unless crlf
112
-
113
- chunk_size = body_buffer.byteslice(offset, crlf - offset).to_i(16)
114
-
115
- if chunk_size == 0
116
- chunked_complete = true
117
- break
118
- end
119
-
120
- offset = crlf + 2
121
- decoded_body << body_buffer.byteslice(offset, chunk_size)
122
- offset += chunk_size + 2
123
- end
124
-
125
- if chunked_complete
230
+ case chunked_state
231
+ when :complete
126
232
  env.delete(HTTP_TRANSFER_ENCODING)
127
233
  data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
234
+ when :too_large
235
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
128
236
  else
129
237
  data.merge(env: env, parse_data: parse_data)
130
238
  end
@@ -155,6 +263,12 @@ module Raptor
155
263
  #
156
264
  # @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
157
265
  def handle_parsed_request(parsed_request, reactor, thread_pool)
266
+ if parsed_request[:too_large]
267
+ socket = reactor.remove(parsed_request[:id])
268
+ reject_oversized(socket) if socket
269
+ return
270
+ end
271
+
158
272
  unless parsed_request[:complete]
159
273
  reactor.update_state(parsed_request)
160
274
  else
@@ -244,10 +358,18 @@ module Raptor
244
358
  keep_alive && !hijacked
245
359
  rescue => error
246
360
  call_response_finished(rack_env, status, headers, error) if rack_env
247
- socket.write(ERROR_RESPONSE_500) rescue nil unless response_started || hijacked
361
+ socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
248
362
  keep_alive = false
249
- raise
363
+
364
+ if @on_error
365
+ @on_error.call(rack_env, error) rescue nil
366
+ else
367
+ raise
368
+ end
250
369
  ensure
370
+ rack_input = rack_env && rack_env[Rack::RACK_INPUT]
371
+ rack_input.close! rescue nil if rack_input.respond_to?(:close!)
372
+
251
373
  unless hijacked || keep_alive
252
374
  socket.close rescue nil
253
375
  end
@@ -278,7 +400,7 @@ module Raptor
278
400
  end
279
401
 
280
402
  data = begin
281
- socket.read_nonblock(KEEPALIVE_BUFFER_SIZE)
403
+ socket.read_nonblock(READ_BUFFER_SIZE)
282
404
  rescue IO::WaitReadable
283
405
  reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
284
406
  return
@@ -346,8 +468,12 @@ module Raptor
346
468
  end
347
469
  end
348
470
 
349
- # Re-registers a socket with the reactor for further processing
350
- # when an incomplete request is received during eager keep-alive.
471
+ # Re-registers a socket with the reactor for further processing when
472
+ # an incomplete request is received during eager accept or eager keep-alive.
473
+ #
474
+ # The persisted flag selects between persistent_data_timeout (for
475
+ # kept-alive connections awaiting the next request) and chunk_data_timeout
476
+ # (for fresh connections awaiting the rest of the first request).
351
477
  #
352
478
  # @param socket [TCPSocket] the client socket
353
479
  # @param id [Integer] unique client identifier
@@ -358,21 +484,35 @@ module Raptor
358
484
  # @param request_count [Integer] number of requests handled on this connection
359
485
  # @param remote_addr [String] client IP address
360
486
  # @param url_scheme [String] "http" or "https"
487
+ # @param persisted [Boolean] whether the connection has already completed at least one request
361
488
  # @return [void]
362
489
  #
363
- # @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> void
364
- def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
490
+ # @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, persisted: bool) -> void
491
+ def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true)
365
492
  reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
366
- reactor.update_state(Ractor.make_shareable({
493
+ state = {
367
494
  id: id,
368
495
  buffer: buffer,
369
496
  env: env,
370
497
  request_count: request_count,
371
498
  parse_data: parse_data,
372
499
  remote_addr: remote_addr,
373
- url_scheme: url_scheme,
374
- persisted: true
375
- }))
500
+ url_scheme: url_scheme
501
+ }
502
+ state[:persisted] = true if persisted
503
+ reactor.update_state(Ractor.make_shareable(state))
504
+ end
505
+
506
+ # Writes a 413 response and closes the socket. Used when a request body
507
+ # exceeds the configured maximum size.
508
+ #
509
+ # @param socket [TCPSocket] the client socket
510
+ # @return [void]
511
+ #
512
+ # @rbs (TCPSocket socket) -> void
513
+ def reject_oversized(socket)
514
+ socket.write(CONTENT_TOO_LARGE_RESPONSE) rescue nil
515
+ socket.close rescue nil
376
516
  end
377
517
 
378
518
  # Builds a Rack environment hash from parsed HTTP request data.
@@ -392,7 +532,7 @@ module Raptor
392
532
  def build_rack_env(env, parse_data, body, socket, remote_addr: "127.0.0.1", url_scheme: HTTP_SCHEME)
393
533
  env[Rack::RACK_VERSION] = Rack::VERSION
394
534
  env[Rack::RACK_URL_SCHEME] = url_scheme
395
- env[Rack::RACK_INPUT] = (body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
535
+ env[Rack::RACK_INPUT] = build_rack_input(body)
396
536
  env[Rack::RACK_ERRORS] = $stderr
397
537
  env[Rack::RACK_RESPONSE_FINISHED] = []
398
538
 
@@ -436,6 +576,26 @@ module Raptor
436
576
  env
437
577
  end
438
578
 
579
+ # Builds the `rack.input` IO object for the request body. Returns an
580
+ # in-memory StringIO for bodies up to the spool threshold, or a Tempfile
581
+ # for larger bodies to bound per-worker memory.
582
+ #
583
+ # @param body [String, nil] decoded request body
584
+ # @return [IO] an IO-like object positioned at the start of the body
585
+ #
586
+ # @rbs (String? body) -> IO
587
+ def build_rack_input(body)
588
+ if body && @body_spool_threshold && body.bytesize > @body_spool_threshold
589
+ tempfile = Tempfile.new("raptor-body")
590
+ tempfile.binmode
591
+ tempfile.write(body)
592
+ tempfile.rewind
593
+ tempfile
594
+ else
595
+ (body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
596
+ end
597
+ end
598
+
439
599
  # Determines whether the connection should be kept alive after the response.
440
600
  #
441
601
  # Returns false if the request limit has been reached. For HTTP/1.1, keep-alive