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.
data/lib/raptor/http2.rb CHANGED
@@ -6,6 +6,7 @@ require "stringio"
6
6
  require "atomic-ruby/atom"
7
7
  require "rack"
8
8
 
9
+ require_relative "request"
9
10
  require_relative "raptor_http2"
10
11
 
11
12
  module Raptor
@@ -68,34 +69,190 @@ module Raptor
68
69
  break if pending.empty?
69
70
 
70
71
  pending.each do |frame|
71
- socket.write(frame) rescue nil
72
+ Request.socket_write(socket, frame) rescue nil
72
73
  end
73
74
  end
74
75
  end
75
76
  end
76
77
 
78
+ # Per-connection outbound flow-control accounting.
79
+ #
80
+ # Tracks the peer's connection-level and per-stream receive windows so
81
+ # outbound DATA frames respect RFC 7540 §5.2. Threads dispatching stream
82
+ # responses call `acquire` to reserve send capacity; threads applying
83
+ # inbound WINDOW_UPDATE or SETTINGS frames call the mutating methods to
84
+ # replenish it. State is held in a single `Atom` so updates use CAS.
85
+ #
86
+ class FlowControl
87
+ ACQUIRE_POLL_INTERVAL = 0.001
88
+
89
+ # @rbs @connection_window: Atom
90
+ # @rbs @stream_windows: Atom
91
+ # @rbs @initial_stream_window: Atom
92
+
93
+ # Creates a new FlowControl with the spec-default windows.
94
+ #
95
+ # @rbs () -> void
96
+ def initialize
97
+ @connection_window = Atom.new(DEFAULT_WINDOW_SIZE)
98
+ @stream_windows = Atom.new({})
99
+ @initial_stream_window = Atom.new(DEFAULT_WINDOW_SIZE)
100
+ end
101
+
102
+ # Reserves outbound capacity on the given stream, polling until at
103
+ # least one byte is available on both the connection and stream
104
+ # windows. The returned size is capped at `MAX_FRAME_SIZE`.
105
+ #
106
+ # When `end_stream` is true, `max_bytes` fits within the peer's
107
+ # initial stream window, and no per-stream override has been
108
+ # recorded, only the connection window is consulted. The stream
109
+ # closes on this frame, so its remaining send window will not be
110
+ # consulted again and need not be tracked.
111
+ #
112
+ # @param stream_id [Integer] the HTTP/2 stream identifier
113
+ # @param max_bytes [Integer] the largest size the caller would like to send
114
+ # @param end_stream [Boolean] true when this is the final frame on the stream
115
+ # @return [Integer] the number of bytes the caller may now send
116
+ #
117
+ # @rbs (Integer stream_id, Integer max_bytes, ?end_stream: bool) -> Integer
118
+ def acquire(stream_id, max_bytes, end_stream: false)
119
+ initial = @initial_stream_window.value
120
+ capped = max_bytes < MAX_FRAME_SIZE ? max_bytes : MAX_FRAME_SIZE
121
+
122
+ if end_stream && capped <= initial && !@stream_windows.value.key?(stream_id)
123
+ loop do
124
+ granted = 0
125
+ @connection_window.swap do |window|
126
+ granted = window > capped ? capped : window
127
+ granted > 0 ? window - granted : window
128
+ end
129
+ return granted if granted > 0
130
+
131
+ sleep ACQUIRE_POLL_INTERVAL
132
+ end
133
+ end
134
+
135
+ loop do
136
+ stream_window = @stream_windows.value[stream_id] || initial
137
+ capped_full = capped < stream_window ? capped : stream_window
138
+
139
+ granted = 0
140
+ if capped_full > 0
141
+ @connection_window.swap do |window|
142
+ granted = window > capped_full ? capped_full : window
143
+ granted > 0 ? window - granted : window
144
+ end
145
+ end
146
+
147
+ if granted > 0
148
+ @stream_windows.swap do |s|
149
+ current = s[stream_id] || initial
150
+ s.merge(stream_id => current - granted)
151
+ end
152
+ return granted
153
+ end
154
+
155
+ sleep ACQUIRE_POLL_INTERVAL
156
+ end
157
+ end
158
+
159
+ # Increments the connection-level send window. Called when the peer
160
+ # sends a WINDOW_UPDATE on stream 0.
161
+ #
162
+ # @param increment [Integer] the byte count to add
163
+ # @return [void]
164
+ #
165
+ # @rbs (Integer increment) -> void
166
+ def add_connection_window(increment)
167
+ @connection_window.swap { |window| window + increment }
168
+ end
169
+
170
+ # Increments the per-stream send window. Called when the peer sends
171
+ # a WINDOW_UPDATE on a specific stream.
172
+ #
173
+ # @param stream_id [Integer] the HTTP/2 stream identifier
174
+ # @param increment [Integer] the byte count to add
175
+ # @return [void]
176
+ #
177
+ # @rbs (Integer stream_id, Integer increment) -> void
178
+ def add_stream_window(stream_id, increment)
179
+ initial = @initial_stream_window.value
180
+ @stream_windows.swap do |s|
181
+ current = s[stream_id] || initial
182
+ s.merge(stream_id => current + increment)
183
+ end
184
+ end
185
+
186
+ # Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
187
+ # existing stream window by the delta as required by RFC 7540 §6.9.2.
188
+ #
189
+ # @param new_size [Integer] the peer's new initial window size
190
+ # @return [void]
191
+ #
192
+ # @rbs (Integer new_size) -> void
193
+ def set_initial_stream_window(new_size)
194
+ old = @initial_stream_window.value
195
+ @initial_stream_window.swap { new_size }
196
+ delta = new_size - old
197
+ return if delta.zero?
198
+
199
+ @stream_windows.swap do |s|
200
+ s.transform_values { |size| size + delta }
201
+ end
202
+ end
203
+
204
+ # Discards any per-stream tracking for the given stream. Called
205
+ # after a stream closes so `@stream_windows` does not grow without
206
+ # bound across the lifetime of a connection.
207
+ #
208
+ # @param stream_id [Integer] the HTTP/2 stream identifier
209
+ # @return [void]
210
+ #
211
+ # @rbs (Integer stream_id) -> void
212
+ def discard_stream(stream_id)
213
+ return unless @stream_windows.value.key?(stream_id)
214
+
215
+ @stream_windows.swap do |s|
216
+ next s unless s.key?(stream_id)
217
+
218
+ new = s.dup
219
+ new.delete(stream_id)
220
+ new
221
+ end
222
+ end
223
+ end
224
+
77
225
  FLAG_END_STREAM = 0x1
78
226
  FLAG_END_HEADERS = 0x4
79
227
  FLAG_ACK = 0x1
80
228
  FLAG_PRIORITY = 0x20
81
229
 
230
+ ERROR_NO_ERROR = 0x0
231
+ ERROR_PROTOCOL_ERROR = 0x1
232
+
233
+ DEFAULT_WINDOW_SIZE = 65_535
234
+ MAX_FRAME_SIZE = 16_384
235
+
82
236
  SERVER_PROTOCOL = "HTTP/2"
83
237
  RACK_HEADER_PREFIX = "rack."
84
238
  HOP_BY_HOP_HEADERS = Set.new(%w[connection transfer-encoding keep-alive upgrade proxy-connection]).freeze
85
239
 
86
240
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
87
241
  # @rbs @server_port: Integer
242
+ # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
88
243
 
89
244
  # Creates a new Http2 handler.
90
245
  #
91
246
  # @param app [#call] the Rack application to dispatch requests to
92
247
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
248
+ # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
93
249
  # @return [void]
94
250
  #
95
- # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
96
- def initialize(app, server_port)
251
+ # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
252
+ def initialize(app, server_port, on_error: nil)
97
253
  @app = app
98
254
  @server_port = server_port
255
+ @on_error = on_error
99
256
  end
100
257
 
101
258
  # Builds the initial server SETTINGS frame to send on connection establishment.
@@ -107,7 +264,7 @@ module Raptor
107
264
  parser = Http2Parser.new
108
265
  settings_payload = parser.build_settings(
109
266
  max_concurrent_streams: 100,
110
- initial_window_size: 65_535
267
+ initial_window_size: DEFAULT_WINDOW_SIZE
111
268
  )
112
269
  parser.build_frame(:settings, 0, 0, settings_payload)
113
270
  end
@@ -129,15 +286,20 @@ module Raptor
129
286
  streams = data[:http2_streams] ? data[:http2_streams].dup : {}
130
287
  outgoing_frames = []
131
288
  completed_requests = []
132
- connection_window = data[:http2_window] || 65_535
289
+ window_updates = []
290
+ peer_initial_window_size = nil
291
+ connection_window = data[:http2_window] || DEFAULT_WINDOW_SIZE
133
292
  preface_received = data[:http2_preface_received] || false
293
+ last_client_stream_id = data[:http2_last_client_stream_id] || 0
294
+ pending_headers = data[:http2_pending_headers]
295
+ goaway_error = nil
134
296
 
135
297
  unless preface_received
136
298
  if buffer.bytesize >= 24 && buffer.byteslice(0, 24) == Http2Parser.connection_preface
137
299
  buffer = buffer.byteslice(24..-1) || ""
138
300
  preface_received = true
139
301
  else
140
- return build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, connection_window, preface_received)
302
+ return build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, false)
141
303
  end
142
304
  end
143
305
 
@@ -148,9 +310,16 @@ module Raptor
148
310
  frame, consumed = parsed
149
311
  buffer = buffer.byteslice(consumed..-1) || ""
150
312
 
313
+ if pending_headers && frame[:type] != :continuation
314
+ goaway_error = ERROR_PROTOCOL_ERROR
315
+ break
316
+ end
317
+
151
318
  case frame[:type]
152
319
  when :settings
153
320
  if (frame[:flags] & FLAG_ACK).zero?
321
+ parsed_settings = parser.parse_settings(frame[:payload])
322
+ peer_initial_window_size = parsed_settings[:initial_window_size] if parsed_settings.key?(:initial_window_size)
154
323
  outgoing_frames << parser.build_frame(:settings, FLAG_ACK, 0, nil)
155
324
  end
156
325
 
@@ -158,37 +327,58 @@ module Raptor
158
327
  stream_id = frame[:stream_id]
159
328
  header_payload = frame[:payload]
160
329
 
330
+ unless streams.key?(stream_id)
331
+ if stream_id.even? || stream_id <= last_client_stream_id
332
+ goaway_error = ERROR_PROTOCOL_ERROR
333
+ break
334
+ end
335
+ last_client_stream_id = stream_id
336
+ end
337
+
161
338
  if (frame[:flags] & FLAG_PRIORITY) != 0
162
339
  header_payload = header_payload.byteslice(5..-1) || ""
163
340
  end
164
341
 
165
- decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
166
- stream = streams[stream_id] || {}
167
- stream = stream.merge(headers: decoded_headers)
168
-
169
- if (frame[:flags] & FLAG_END_STREAM) != 0
170
- stream = stream.merge(end_stream: true)
171
- completed_requests << {
172
- stream_id: stream_id,
173
- headers: decoded_headers,
174
- body: stream[:body] || ""
175
- }
342
+ end_stream = (frame[:flags] & FLAG_END_STREAM) != 0
176
343
 
177
- streams.delete(stream_id)
344
+ if (frame[:flags] & FLAG_END_HEADERS) != 0
345
+ decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
346
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
178
347
  else
179
- streams[stream_id] = stream
348
+ pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream }
349
+ end
350
+
351
+ when :continuation
352
+ if pending_headers.nil? || frame[:stream_id] != pending_headers[:stream_id]
353
+ goaway_error = ERROR_PROTOCOL_ERROR
354
+ break
355
+ end
356
+
357
+ pending_headers = pending_headers.merge(buffer: pending_headers[:buffer] + frame[:payload])
358
+
359
+ if (frame[:flags] & FLAG_END_HEADERS) != 0
360
+ stream_id = pending_headers[:stream_id]
361
+ decoded_headers, hpack_table = parser.parse_headers(pending_headers[:buffer], hpack_table)
362
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream])
363
+ pending_headers = nil
180
364
  end
181
365
 
182
366
  when :data
183
367
  stream_id = frame[:stream_id]
184
- stream = streams[stream_id] || {}
368
+
369
+ unless streams.key?(stream_id)
370
+ goaway_error = ERROR_PROTOCOL_ERROR
371
+ break
372
+ end
373
+
374
+ stream = streams[stream_id]
185
375
  existing_body = stream[:body] || ""
186
376
  stream = stream.merge(body: existing_body + frame[:payload])
187
377
 
188
378
  if frame[:payload].bytesize.positive?
189
379
  connection_window -= frame[:payload].bytesize
190
- if connection_window < 32_768
191
- increment = 65_535 - connection_window
380
+ if connection_window < DEFAULT_WINDOW_SIZE / 2
381
+ increment = DEFAULT_WINDOW_SIZE - connection_window
192
382
  wu_payload = [increment].pack("N")
193
383
  outgoing_frames << parser.build_frame(:window_update, 0, 0, wu_payload)
194
384
  outgoing_frames << parser.build_frame(:window_update, 0, stream_id, wu_payload)
@@ -210,7 +400,8 @@ module Raptor
210
400
  end
211
401
 
212
402
  when :window_update
213
- parser.parse_window_update(frame[:payload])
403
+ increment = parser.parse_window_update(frame[:payload])
404
+ window_updates << [frame[:stream_id], increment]
214
405
 
215
406
  when :ping
216
407
  if (frame[:flags] & FLAG_ACK).zero?
@@ -225,8 +416,44 @@ module Raptor
225
416
  end
226
417
  end
227
418
 
228
- build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, connection_window, preface_received)
419
+ if goaway_error
420
+ goaway_payload = [last_client_stream_id, goaway_error].pack("NN")
421
+ outgoing_frames << parser.build_frame(:goaway, 0, 0, goaway_payload)
422
+ end
423
+
424
+ build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, !goaway_error.nil?)
425
+ end
426
+
427
+ # Merges a decoded header block into the stream's accumulated state,
428
+ # promoting the stream to `completed_requests` when END_STREAM is set.
429
+ #
430
+ # @param streams [Hash] current open-stream map
431
+ # @param completed_requests [Array<Hash>] accumulator of completed stream requests
432
+ # @param stream_id [Integer] the stream identifier
433
+ # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
434
+ # @param end_stream [Boolean] whether the source frame had END_STREAM set
435
+ # @return [Array(Hash, Array<Hash>)] updated streams and completed_requests
436
+ #
437
+ # @rbs (Hash[Integer, Hash[Symbol, untyped]] streams, Array[Hash[Symbol, untyped]] completed_requests, Integer stream_id, Array[[String, String]] decoded_headers, bool end_stream) -> [Hash[Integer, Hash[Symbol, untyped]], Array[Hash[Symbol, untyped]]]
438
+ def self.finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
439
+ stream = streams[stream_id] || {}
440
+ stream = stream.merge(headers: decoded_headers)
441
+
442
+ if end_stream
443
+ completed_requests << {
444
+ stream_id: stream_id,
445
+ headers: decoded_headers,
446
+ body: stream[:body] || ""
447
+ }
448
+
449
+ streams.delete(stream_id)
450
+ else
451
+ streams[stream_id] = stream
452
+ end
453
+
454
+ [streams, completed_requests]
229
455
  end
456
+ private_class_method :finalize_headers
230
457
 
231
458
  # Builds a frozen result hash from the current processing state.
232
459
  #
@@ -236,13 +463,18 @@ module Raptor
236
463
  # @param streams [Hash] updated stream states
237
464
  # @param outgoing_frames [Array<String>] frames to write to the socket
238
465
  # @param completed_requests [Array<Hash>] fully received stream requests
466
+ # @param window_updates [Array<Array(Integer, Integer)>] inbound WINDOW_UPDATE pairs as [stream_id, increment]
467
+ # @param peer_initial_window_size [Integer, nil] new SETTINGS_INITIAL_WINDOW_SIZE announced by the peer
239
468
  # @param connection_window [Integer] current connection flow control window
240
469
  # @param preface_received [Boolean] whether the connection preface has been received
470
+ # @param last_client_stream_id [Integer] highest client-initiated stream ID seen
471
+ # @param pending_headers [Hash, nil] in-progress HEADERS+CONTINUATION assembly
472
+ # @param close_connection [Boolean] whether the connection should be closed after writing outgoing frames
241
473
  # @return [Hash] frozen result hash
242
474
  #
243
- # @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Integer connection_window, bool preface_received) -> Hash[Symbol, untyped]
244
- def self.build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, connection_window, preface_received)
245
- Ractor.make_shareable({
475
+ # @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[[Integer, Integer]] window_updates, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
476
+ def self.build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, close_connection)
477
+ result = {
246
478
  id: data[:id],
247
479
  protocol: :http2,
248
480
  buffer: buffer || "",
@@ -250,11 +482,17 @@ module Raptor
250
482
  http2_streams: streams,
251
483
  http2_window: connection_window,
252
484
  http2_preface_received: preface_received,
485
+ http2_last_client_stream_id: last_client_stream_id,
486
+ http2_pending_headers: pending_headers,
253
487
  outgoing_frames: outgoing_frames,
254
488
  completed_requests: completed_requests,
489
+ close_connection: close_connection,
255
490
  remote_addr: data[:remote_addr],
256
491
  url_scheme: data[:url_scheme]
257
- })
492
+ }
493
+ result[:window_updates] = window_updates unless window_updates.empty?
494
+ result[:peer_initial_window_size] = peer_initial_window_size if peer_initial_window_size
495
+ Ractor.make_shareable(result)
258
496
  end
259
497
  private_class_method :build_result
260
498
 
@@ -274,9 +512,19 @@ module Raptor
274
512
  return unless socket
275
513
 
276
514
  writer = reactor.writer_for(result[:id])
515
+ flow_control = reactor.flow_control_for(result[:id])
516
+
517
+ if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
518
+ apply_flow_control_updates(flow_control, result)
519
+ end
277
520
 
278
521
  writer.write_frames(socket, result[:outgoing_frames])
279
522
 
523
+ if result[:close_connection]
524
+ reactor.close_connection(result[:id])
525
+ return
526
+ end
527
+
280
528
  reactor.update_http2_state(result)
281
529
 
282
530
  result[:completed_requests]&.each do |request|
@@ -285,7 +533,7 @@ module Raptor
285
533
 
286
534
  thread_pool << proc do
287
535
  dispatch_stream_request(
288
- socket, writer, stream_id,
536
+ socket, writer, flow_control, stream_id,
289
537
  request[:headers], request[:body],
290
538
  remote_addr: remote_addr
291
539
  )
@@ -295,42 +543,75 @@ module Raptor
295
543
 
296
544
  private
297
545
 
546
+ # Applies inbound flow-control updates from a parsed result to the
547
+ # connection's `FlowControl`.
548
+ #
549
+ # @param flow_control [FlowControl] the per-connection flow controller
550
+ # @param result [Hash] the parsed result from `process_frames`
551
+ # @return [void]
552
+ #
553
+ # @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
554
+ def apply_flow_control_updates(flow_control, result)
555
+ result[:window_updates]&.each do |stream_id, increment|
556
+ if stream_id.zero?
557
+ flow_control.add_connection_window(increment)
558
+ else
559
+ flow_control.add_stream_window(stream_id, increment)
560
+ end
561
+ end
562
+
563
+ if (new_size = result[:peer_initial_window_size])
564
+ flow_control.set_initial_stream_window(new_size)
565
+ end
566
+ end
567
+
298
568
  # Dispatches a completed stream request to the Rack app and writes
299
569
  # the response back as HTTP/2 frames.
300
570
  #
301
571
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
302
572
  # @param writer [Writer] lock-free frame writer for the connection
573
+ # @param flow_control [FlowControl] per-connection outbound flow controller
303
574
  # @param stream_id [Integer] the HTTP/2 stream identifier
304
575
  # @param headers [Array<Array(String, String)>] request headers
305
576
  # @param body [String] request body
306
577
  # @param remote_addr [String] the client IP address
307
578
  # @return [void]
308
579
  #
309
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
310
- def dispatch_stream_request(socket, writer, stream_id, headers, body, remote_addr:)
580
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
581
+ def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
311
582
  env = build_rack_env(headers, body, remote_addr: remote_addr)
312
583
  status, response_headers, response_body = @app.call(env)
313
584
 
314
- write_http2_response(socket, writer, stream_id, status, response_headers, response_body)
315
- rescue
585
+ write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body)
586
+ rescue => error
316
587
  write_http2_error_response(socket, writer, stream_id)
317
- raise
588
+
589
+ if @on_error
590
+ @on_error.call(env, error) rescue nil
591
+ else
592
+ raise
593
+ end
318
594
  ensure
319
595
  response_body.close if response_body.respond_to?(:close)
596
+ flow_control.discard_stream(stream_id) if flow_control
320
597
  end
321
598
 
322
599
  # Writes a Rack response as HTTP/2 frames to the socket.
323
600
  #
601
+ # DATA frames are partitioned through `flow_control` so each write fits
602
+ # within the peer's per-stream and connection windows.
603
+ #
324
604
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
325
605
  # @param writer [Writer] lock-free frame writer for the connection
606
+ # @param flow_control [FlowControl] per-connection outbound flow controller
326
607
  # @param stream_id [Integer] the HTTP/2 stream identifier
327
608
  # @param status [Integer] HTTP status code
328
609
  # @param headers [Hash] response headers from the Rack application
329
610
  # @param body [Object] response body responding to each
330
611
  # @return [void]
331
612
  #
332
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
333
- def write_http2_response(socket, writer, stream_id, status, headers, body)
613
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
614
+ def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
334
615
  parser = Http2Parser.new
335
616
 
336
617
  header_pairs = [[":status", status.to_s]]
@@ -350,16 +631,24 @@ module Raptor
350
631
  body_chunks = []
351
632
  body.each { |chunk| body_chunks << chunk unless chunk.empty? }
352
633
 
353
- frames = []
354
634
  if body_chunks.empty?
355
- frames << parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)
356
- else
357
- frames << parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)
635
+ writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
636
+ return
637
+ end
358
638
 
359
- last_index = body_chunks.size - 1
360
- body_chunks.each_with_index do |chunk, index|
361
- flags = index == last_index ? FLAG_END_STREAM : 0
362
- frames << parser.build_frame(:data, flags, stream_id, chunk)
639
+ frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)]
640
+
641
+ last_chunk_index = body_chunks.size - 1
642
+ body_chunks.each_with_index do |chunk, chunk_index|
643
+ offset = 0
644
+ while offset < chunk.bytesize
645
+ remaining = chunk.bytesize - offset
646
+ last_frame = chunk_index == last_chunk_index && remaining <= MAX_FRAME_SIZE
647
+ granted = flow_control.acquire(stream_id, remaining, end_stream: last_frame)
648
+ slice = offset == 0 && granted == chunk.bytesize ? chunk : chunk.byteslice(offset, granted)
649
+ offset += granted
650
+ end_stream = chunk_index == last_chunk_index && offset == chunk.bytesize
651
+ frames << parser.build_frame(:data, end_stream ? FLAG_END_STREAM : 0, stream_id, slice)
363
652
  end
364
653
  end
365
654