raptor 0.2.0 → 0.4.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.
@@ -76,6 +76,7 @@ module Raptor
76
76
  # @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
77
77
  # @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
78
78
  # @rbs @id_to_writer: Hash[Integer, untyped]
79
+ # @rbs @id_to_flow_control: Hash[Integer, untyped]
79
80
 
80
81
  # Creates a new Reactor instance.
81
82
  #
@@ -101,6 +102,7 @@ module Raptor
101
102
  @socket_to_state = {}
102
103
  @id_to_timeout = {}
103
104
  @id_to_writer = {}
105
+ @id_to_flow_control = {}
104
106
  end
105
107
 
106
108
  # Starts the reactor's main event loop in a new thread.
@@ -117,33 +119,38 @@ module Raptor
117
119
  Thread.current.name = self.class.name
118
120
 
119
121
  until @queue.closed? && @queue.empty?
120
- timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
121
- @selector.select(timeout) do |monitor|
122
- wakeup!(monitor.value)
123
- end
124
-
125
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
- expired = []
127
- @timeouts.traverse do |to_client|
128
- break unless to_client.timeout(now) == 0
129
-
130
- expired << to_client
131
- end
132
-
133
- expired.each do |to_client|
134
- @timeouts.delete!(to_client)
135
- id = to_client.client_data[:id]
136
- @id_to_timeout.delete(id)
137
- socket = @id_to_socket[id]
138
- next unless socket
139
-
140
- @selector.deregister(socket)
141
- socket.write(TIMEOUT_RESPONSE) rescue nil
142
- cleanup(socket)
143
- end
144
-
145
- until @queue.empty?
146
- register(@queue.pop)
122
+ begin
123
+ timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
124
+ @selector.select(timeout) do |monitor|
125
+ wakeup!(monitor.value)
126
+ end
127
+
128
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
129
+ expired = []
130
+ @timeouts.traverse do |to_client|
131
+ break unless to_client.timeout(now) == 0
132
+
133
+ expired << to_client
134
+ end
135
+
136
+ expired.each do |to_client|
137
+ @timeouts.delete!(to_client)
138
+ id = to_client.client_data[:id]
139
+ @id_to_timeout.delete(id)
140
+ socket = @id_to_socket[id]
141
+ next unless socket
142
+
143
+ @selector.deregister(socket)
144
+ socket.write(TIMEOUT_RESPONSE) rescue nil
145
+ cleanup(socket)
146
+ end
147
+
148
+ until @queue.empty?
149
+ register(@queue.pop)
150
+ end
151
+ rescue => error
152
+ warn "#{Thread.current.name} rescued:"
153
+ warn error.full_message
147
154
  end
148
155
  end
149
156
 
@@ -163,9 +170,11 @@ module Raptor
163
170
  socket = state[:socket]
164
171
  state.delete(:socket)
165
172
  writer = state.delete(:writer)
173
+ flow_control = state.delete(:flow_control)
166
174
  @id_to_socket[state[:id]] = socket
167
175
  @socket_to_state[socket] = state
168
176
  @id_to_writer[state[:id]] = writer if writer
177
+ @id_to_flow_control[state[:id]] = flow_control if flow_control
169
178
 
170
179
  read_and_queue_for_parse(socket, state)
171
180
  end
@@ -262,6 +271,18 @@ module Raptor
262
271
  @id_to_writer[id]
263
272
  end
264
273
 
274
+ # Returns the flow controller associated with a given connection, if one
275
+ # was supplied when the connection was added. Used by HTTP/2 stream
276
+ # dispatchers to honour the peer's flow-control windows.
277
+ #
278
+ # @param id [Integer] unique client identifier
279
+ # @return [Object, nil] the flow controller, if found
280
+ #
281
+ # @rbs (Integer id) -> untyped?
282
+ def flow_control_for(id)
283
+ @id_to_flow_control[id]
284
+ end
285
+
265
286
  # Updates connection state for an HTTP/2 connection after frame processing.
266
287
  #
267
288
  # Re-registers the socket with the selector for further reads and stores
@@ -282,6 +303,24 @@ module Raptor
282
303
  socket.close
283
304
  end
284
305
 
306
+ # Closes the socket for the given connection and drops all reactor state
307
+ # associated with it. Used to terminate HTTP/2 connections after sending
308
+ # a GOAWAY frame.
309
+ #
310
+ # @param id [Integer] unique client identifier
311
+ # @return [void]
312
+ #
313
+ # @rbs (Integer id) -> void
314
+ def close_connection(id)
315
+ socket = @id_to_socket.delete(id)
316
+ return unless socket
317
+
318
+ @socket_to_state.delete(socket)
319
+ @id_to_writer.delete(id)
320
+ @id_to_flow_control.delete(id)
321
+ socket.close rescue nil
322
+ end
323
+
285
324
  # Initiates reactor shutdown.
286
325
  #
287
326
  # Closes the registration queue and wakes up the selector to begin
@@ -385,6 +424,7 @@ module Raptor
385
424
  state = @socket_to_state.delete(socket)
386
425
  @id_to_socket.delete(state[:id])
387
426
  @id_to_writer.delete(state[:id])
427
+ @id_to_flow_control.delete(state[:id])
388
428
  socket.close
389
429
  end
390
430
 
@@ -3,7 +3,9 @@
3
3
 
4
4
  require "socket"
5
5
  require "stringio"
6
+ require "tempfile"
6
7
 
8
+ require "atomic-ruby/atomic_boolean"
7
9
  require "rack"
8
10
 
9
11
  require_relative "raptor_http"
@@ -38,7 +40,9 @@ module Raptor
38
40
  end
39
41
 
40
42
  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"
43
+ BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
44
+ INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
45
+ CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
42
46
 
43
47
  CONNECTION_CLOSE = "close"
44
48
  CONNECTION_KEEPALIVE = "keep-alive"
@@ -61,46 +65,97 @@ module Raptor
61
65
 
62
66
  # Decodes a chunked transfer-encoded body buffer.
63
67
  #
64
- # Returns the decoded bytes and a flag indicating whether the terminating
65
- # zero-length chunk was found. The decoder stops at the first unparseable
66
- # boundary (incomplete CRLF) or zero-length chunk.
68
+ # Returns the decoded bytes and a state symbol: `:complete` when the
69
+ # terminating zero-length chunk was found, `:too_large` when the decoded
70
+ # size would exceed `max_size`, or `:incomplete` otherwise.
67
71
  #
68
72
  # @param buffer [String] the raw body buffer to decode
69
- # @return [Array(String, Boolean)] decoded body and completion flag
73
+ # @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
74
+ # @return [Array(String, Symbol)] decoded body and completion state
70
75
  #
71
- # @rbs (String buffer) -> [String, bool]
72
- def self.decode_chunked(buffer)
76
+ # @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
77
+ def self.decode_chunked(buffer, max_size = nil)
73
78
  decoded = String.new
74
79
  offset = 0
75
80
 
76
81
  while offset < buffer.bytesize
77
82
  crlf = buffer.index("\r\n", offset)
78
- return [decoded, false] unless crlf
83
+ return [decoded, :incomplete] unless crlf
79
84
 
80
85
  chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
81
- return [decoded, true] if chunk_size == 0
86
+ return [decoded, :complete] if chunk_size == 0
87
+ return [decoded, :too_large] if max_size && decoded.bytesize + chunk_size > max_size
82
88
 
83
89
  offset = crlf + 2
84
90
  decoded << buffer.byteslice(offset, chunk_size)
85
91
  offset += chunk_size + 2
86
92
  end
87
93
 
88
- [decoded, false]
94
+ [decoded, :incomplete]
95
+ end
96
+
97
+ # Writes a string to the socket, retrying on partial writes and flow control blocks.
98
+ #
99
+ # Uses write_nonblock with `WRITE_TIMEOUT` to avoid blocking the thread
100
+ # indefinitely on slow clients.
101
+ #
102
+ # @param socket [TCPSocket] the socket to write to
103
+ # @param string [String] the data to write
104
+ # @return [void]
105
+ # @raise [WriteError] if the socket is not writable within the timeout or raises IOError
106
+ #
107
+ # @rbs (TCPSocket socket, String string) -> void
108
+ def self.socket_write(socket, string)
109
+ bytes = 0
110
+ byte_size = string.bytesize
111
+
112
+ while bytes < byte_size
113
+ begin
114
+ bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
115
+ rescue IO::WaitWritable
116
+ raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
117
+ retry
118
+ rescue IOError
119
+ raise WriteError
120
+ end
121
+ end
89
122
  end
90
123
 
91
124
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
92
125
  # @rbs @server_port: Integer
126
+ # @rbs @max_body_size: Integer?
127
+ # @rbs @body_spool_threshold: Integer?
128
+ # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
129
+ # @rbs @running: AtomicBoolean
93
130
 
94
131
  # Creates a new Request handler.
95
132
  #
96
133
  # @param app [#call] the Rack application to dispatch complete requests to
97
134
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
135
+ # @param client_options [Hash] client limits configuration
136
+ # @option client_options [Integer, nil] :max_body_size maximum request body size in bytes
137
+ # @option client_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
138
+ # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
98
139
  # @return [void]
99
140
  #
100
- # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
101
- def initialize(app, server_port)
141
+ # @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
142
+ def initialize(app, server_port, client_options: {}, on_error: nil)
102
143
  @app = app
103
144
  @server_port = server_port
145
+ @max_body_size = client_options[:max_body_size]
146
+ @body_spool_threshold = client_options[:body_spool_threshold]
147
+ @on_error = on_error
148
+ @running = AtomicBoolean.new(true)
149
+ end
150
+
151
+ # Signals eager keep-alive loops to stop processing further requests on
152
+ # their connections. In-flight requests complete normally.
153
+ #
154
+ # @return [void]
155
+ #
156
+ # @rbs () -> void
157
+ def shutdown
158
+ @running.make_false
104
159
  end
105
160
 
106
161
  # Eagerly reads and parses the first request on a freshly accepted
@@ -141,7 +196,12 @@ module Raptor
141
196
 
142
197
  parser = HttpParser.new
143
198
  env = {}
144
- nread = parser.execute(env, buffer, 0)
199
+ nread = begin
200
+ parser.execute(env, buffer, 0)
201
+ rescue HttpParserError
202
+ reject_malformed(socket)
203
+ return
204
+ end
145
205
  parse_data = { parse_count: 1, content_length: parser.content_length }
146
206
 
147
207
  body = nil
@@ -149,12 +209,21 @@ module Raptor
149
209
  fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
150
210
  return
151
211
  elsif parser.has_body?
212
+ if @max_body_size && parser.content_length > @max_body_size
213
+ reject_oversized(socket)
214
+ return
215
+ end
216
+
152
217
  body = buffer.byteslice(nread..-1) || ""
153
218
 
154
219
  if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
155
- body, chunked_complete = Request.decode_chunked(body)
156
- if chunked_complete
220
+ body, chunked_state = Request.decode_chunked(body, @max_body_size)
221
+ case chunked_state
222
+ when :complete
157
223
  env.delete(HTTP_TRANSFER_ENCODING)
224
+ when :too_large
225
+ reject_oversized(socket)
226
+ return
158
227
  else
159
228
  fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
160
229
  return
@@ -180,12 +249,18 @@ module Raptor
180
249
  #
181
250
  # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
182
251
  def http_parser_worker
252
+ max_body_size = @max_body_size
253
+
183
254
  proc do |data|
184
255
  next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
185
256
 
186
257
  parser = Raptor::HttpParser.new
187
258
  env = {}
188
- nread = parser.execute(env, data[:buffer], 0)
259
+ nread = begin
260
+ parser.execute(env, data[:buffer], 0)
261
+ rescue Raptor::HttpParserError
262
+ next Ractor.make_shareable(data.merge(complete: true, malformed: true))
263
+ end
189
264
  parse_data = if data[:parse_data]
190
265
  data[:parse_data].dup
191
266
  else
@@ -197,12 +272,17 @@ module Raptor
197
272
  if parser.has_body?
198
273
  body_buffer = data[:buffer].byteslice(nread..-1) || ""
199
274
 
200
- if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
201
- decoded_body, chunked_complete = Raptor::Request.decode_chunked(body_buffer)
275
+ if max_body_size && parser.content_length > max_body_size
276
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
277
+ elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
278
+ decoded_body, chunked_state = Raptor::Request.decode_chunked(body_buffer, max_body_size)
202
279
 
203
- if chunked_complete
280
+ case chunked_state
281
+ when :complete
204
282
  env.delete(HTTP_TRANSFER_ENCODING)
205
283
  data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
284
+ when :too_large
285
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
206
286
  else
207
287
  data.merge(env: env, parse_data: parse_data)
208
288
  end
@@ -233,6 +313,18 @@ module Raptor
233
313
  #
234
314
  # @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
235
315
  def handle_parsed_request(parsed_request, reactor, thread_pool)
316
+ if parsed_request[:too_large]
317
+ socket = reactor.remove(parsed_request[:id])
318
+ reject_oversized(socket) if socket
319
+ return
320
+ end
321
+
322
+ if parsed_request[:malformed]
323
+ socket = reactor.remove(parsed_request[:id])
324
+ reject_malformed(socket) if socket
325
+ return
326
+ end
327
+
236
328
  unless parsed_request[:complete]
237
329
  reactor.update_state(parsed_request)
238
330
  else
@@ -322,10 +414,18 @@ module Raptor
322
414
  keep_alive && !hijacked
323
415
  rescue => error
324
416
  call_response_finished(rack_env, status, headers, error) if rack_env
325
- socket.write(ERROR_RESPONSE_500) rescue nil unless response_started || hijacked
417
+ socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
326
418
  keep_alive = false
327
- raise
419
+
420
+ if @on_error
421
+ @on_error.call(rack_env, error) rescue nil
422
+ else
423
+ raise
424
+ end
328
425
  ensure
426
+ rack_input = rack_env && rack_env[Rack::RACK_INPUT]
427
+ rack_input.close! rescue nil if rack_input.respond_to?(:close!)
428
+
329
429
  unless hijacked || keep_alive
330
430
  socket.close rescue nil
331
431
  end
@@ -350,6 +450,11 @@ module Raptor
350
450
  # @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
351
451
  def eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme)
352
452
  loop do
453
+ unless @running.true?
454
+ socket.close rescue nil
455
+ return
456
+ end
457
+
353
458
  unless socket.wait_readable(KEEPALIVE_READ_TIMEOUT)
354
459
  reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
355
460
  return
@@ -374,7 +479,12 @@ module Raptor
374
479
 
375
480
  parser = HttpParser.new
376
481
  env = {}
377
- nread = parser.execute(env, buffer, 0)
482
+ nread = begin
483
+ parser.execute(env, buffer, 0)
484
+ rescue HttpParserError
485
+ reject_malformed(socket)
486
+ return
487
+ end
378
488
  parse_data = { parse_count: 1, content_length: parser.content_length }
379
489
 
380
490
  body = nil
@@ -459,6 +569,30 @@ module Raptor
459
569
  reactor.update_state(Ractor.make_shareable(state))
460
570
  end
461
571
 
572
+ # Writes a 413 response and closes the socket. Used when a request body
573
+ # exceeds the configured maximum size.
574
+ #
575
+ # @param socket [TCPSocket] the client socket
576
+ # @return [void]
577
+ #
578
+ # @rbs (TCPSocket socket) -> void
579
+ def reject_oversized(socket)
580
+ socket.write(CONTENT_TOO_LARGE_RESPONSE) rescue nil
581
+ socket.close rescue nil
582
+ end
583
+
584
+ # Writes a 400 response and closes the socket. Used when the HTTP parser
585
+ # rejects the request line or headers.
586
+ #
587
+ # @param socket [TCPSocket] the client socket
588
+ # @return [void]
589
+ #
590
+ # @rbs (TCPSocket socket) -> void
591
+ def reject_malformed(socket)
592
+ socket.write(BAD_REQUEST_RESPONSE) rescue nil
593
+ socket.close rescue nil
594
+ end
595
+
462
596
  # Builds a Rack environment hash from parsed HTTP request data.
463
597
  #
464
598
  # Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
@@ -476,7 +610,7 @@ module Raptor
476
610
  def build_rack_env(env, parse_data, body, socket, remote_addr: "127.0.0.1", url_scheme: HTTP_SCHEME)
477
611
  env[Rack::RACK_VERSION] = Rack::VERSION
478
612
  env[Rack::RACK_URL_SCHEME] = url_scheme
479
- env[Rack::RACK_INPUT] = (body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
613
+ env[Rack::RACK_INPUT] = build_rack_input(body)
480
614
  env[Rack::RACK_ERRORS] = $stderr
481
615
  env[Rack::RACK_RESPONSE_FINISHED] = []
482
616
 
@@ -520,6 +654,26 @@ module Raptor
520
654
  env
521
655
  end
522
656
 
657
+ # Builds the `rack.input` IO object for the request body. Returns an
658
+ # in-memory StringIO for bodies up to the spool threshold, or a Tempfile
659
+ # for larger bodies to bound per-worker memory.
660
+ #
661
+ # @param body [String, nil] decoded request body
662
+ # @return [IO] an IO-like object positioned at the start of the body
663
+ #
664
+ # @rbs (String? body) -> IO
665
+ def build_rack_input(body)
666
+ if body && @body_spool_threshold && body.bytesize > @body_spool_threshold
667
+ tempfile = Tempfile.new("raptor-body")
668
+ tempfile.binmode
669
+ tempfile.write(body)
670
+ tempfile.rewind
671
+ tempfile
672
+ else
673
+ (body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
674
+ end
675
+ end
676
+
523
677
  # Determines whether the connection should be kept alive after the response.
524
678
  #
525
679
  # Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
@@ -568,7 +722,7 @@ module Raptor
568
722
  end
569
723
  response << "\r\n"
570
724
 
571
- socket_write(socket, response)
725
+ Request.socket_write(socket, response)
572
726
  end
573
727
 
574
728
  # Writes a complete HTTP response to the socket.
@@ -701,7 +855,7 @@ module Raptor
701
855
  def write_hijacked_response(socket, response, headers, response_hijack)
702
856
  response << format_headers(headers)
703
857
  response << "\r\n"
704
- socket_write(socket, response)
858
+ Request.socket_write(socket, response)
705
859
  uncork_socket(socket)
706
860
  response_hijack.call(socket)
707
861
  end
@@ -726,7 +880,7 @@ module Raptor
726
880
 
727
881
  response << format_headers(headers)
728
882
  response << "\r\n"
729
- socket_write(socket, response)
883
+ Request.socket_write(socket, response)
730
884
  end
731
885
 
732
886
  # Writes a complete response with a body.
@@ -748,7 +902,7 @@ module Raptor
748
902
  if body.respond_to?(:call)
749
903
  response << format_headers(headers)
750
904
  response << "\r\n"
751
- socket_write(socket, response)
905
+ Request.socket_write(socket, response)
752
906
  uncork_socket(socket)
753
907
  body.call(socket)
754
908
  return
@@ -785,7 +939,7 @@ module Raptor
785
939
  raise TypeError, "body must respond to each, to_ary, or to_path"
786
940
  end
787
941
 
788
- socket_write(socket, "0\r\n\r\n") if use_chunked
942
+ Request.socket_write(socket, "0\r\n\r\n") if use_chunked
789
943
  end
790
944
 
791
945
  # Calculates content length from an array or file body without consuming it.
@@ -825,15 +979,15 @@ module Raptor
825
979
  def write_file_body(socket, response, path, content_length, use_chunked)
826
980
  File.open(path, "rb") do |file|
827
981
  if use_chunked
828
- socket_write(socket, response)
982
+ Request.socket_write(socket, response)
829
983
  while (chunk = file.read(FILE_CHUNK_SIZE))
830
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
984
+ Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
831
985
  end
832
986
  elsif content_length && content_length < BODY_BUFFER_THRESHOLD
833
987
  response << file.read(content_length)
834
- socket_write(socket, response)
988
+ Request.socket_write(socket, response)
835
989
  else
836
- socket_write(socket, response)
990
+ Request.socket_write(socket, response)
837
991
  IO.copy_stream(file, socket)
838
992
  end
839
993
  end
@@ -876,12 +1030,12 @@ module Raptor
876
1030
 
877
1031
  if use_chunked
878
1032
  response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
879
- socket_write(socket, response)
1033
+ Request.socket_write(socket, response)
880
1034
  elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
881
- socket_write(socket, response << chunk)
1035
+ Request.socket_write(socket, response << chunk)
882
1036
  else
883
- socket_write(socket, response)
884
- socket_write(socket, chunk)
1037
+ Request.socket_write(socket, response)
1038
+ Request.socket_write(socket, chunk)
885
1039
  end
886
1040
  end
887
1041
 
@@ -897,13 +1051,13 @@ module Raptor
897
1051
  # @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
898
1052
  def write_multiple_chunks(socket, response, body_array, use_chunked)
899
1053
  if use_chunked
900
- socket_write(socket, response)
1054
+ Request.socket_write(socket, response)
901
1055
  body_array.each do |chunk|
902
1056
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
903
1057
 
904
1058
  next if chunk.empty?
905
1059
 
906
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1060
+ Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
907
1061
  end
908
1062
  else
909
1063
  body_array.each do |chunk|
@@ -911,7 +1065,7 @@ module Raptor
911
1065
 
912
1066
  response << chunk
913
1067
  end
914
- socket_write(socket, response)
1068
+ Request.socket_write(socket, response)
915
1069
  end
916
1070
  end
917
1071
 
@@ -927,13 +1081,13 @@ module Raptor
927
1081
  # @rbs (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
928
1082
  def write_enumerable_body(socket, response, body, use_chunked)
929
1083
  if use_chunked
930
- socket_write(socket, response)
1084
+ Request.socket_write(socket, response)
931
1085
  body.each do |chunk|
932
1086
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
933
1087
 
934
1088
  next if chunk.empty?
935
1089
 
936
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1090
+ Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
937
1091
  end
938
1092
  else
939
1093
  body.each do |chunk|
@@ -941,7 +1095,7 @@ module Raptor
941
1095
 
942
1096
  response << chunk
943
1097
  end
944
- socket_write(socket, response)
1098
+ Request.socket_write(socket, response)
945
1099
  end
946
1100
  end
947
1101
 
@@ -1014,33 +1168,6 @@ module Raptor
1014
1168
  end
1015
1169
  end
1016
1170
 
1017
- # Writes a string to the socket, retrying on partial writes and flow control blocks.
1018
- #
1019
- # Uses write_nonblock with a 5-second writable timeout to avoid blocking the
1020
- # thread indefinitely on slow clients.
1021
- #
1022
- # @param socket [TCPSocket] the socket to write to
1023
- # @param string [String] the data to write
1024
- # @return [void]
1025
- # @raise [WriteError] if the socket is not writable within the timeout or raises IOError
1026
- #
1027
- # @rbs (TCPSocket socket, String string) -> void
1028
- def socket_write(socket, string)
1029
- bytes = 0
1030
- byte_size = string.bytesize
1031
-
1032
- while bytes < byte_size
1033
- begin
1034
- bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
1035
- rescue IO::WaitWritable
1036
- raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
1037
- retry
1038
- rescue IOError
1039
- raise WriteError
1040
- end
1041
- end
1042
- end
1043
-
1044
1171
  if Socket.const_defined?(:TCP_CORK)
1045
1172
  # Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.
1046
1173
  #