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