http-2 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c96822381e28e32127984c2dbb7b25b3ad5dd87d69283a752378a9f8778c72ae
4
- data.tar.gz: beb68a1eb2670d1d0b742224b5ad65bd2bc299656433b120ff31299a481297ce
3
+ metadata.gz: 8c3e5ca05eb466a1e2f6c0895ccd5827987cfb8bf8ddbefeeb74210e0e6e3e97
4
+ data.tar.gz: 8589dab8b8c89aefc5067206219ebcac9bffbc185da25d89a5f6dccc11aafcd8
5
5
  SHA512:
6
- metadata.gz: 173e0163e2b05ff3eefdfcc420f8c3913c97702873964ef6c653f0329726443c4c10d73d7ffff31583b06567ef12c8f6545eae0f3d32c52d9b13d98322c09e32
7
- data.tar.gz: 43b38b2bdd3ed6ac90382b98c96b03fd76edefbbf4a385c67c3c8db67e6f755382f09c8a3af848a1d8ed55335ad2925d05793ad71c16c5bb831b07f86e0228be
6
+ metadata.gz: d66c154674cdd29924c0c169ce60c8eeda6ba833b435b2db74b787e86bab101a7f9748f4fb632ecbecf32aaa216fdf1cd9e8d73c333cd501f454b52d0628c6fe
7
+ data.tar.gz: 68f86eca1a92474fd7802502af0883595b52afe42c49c975ea6952e79a523c59508ad5213d18f77f9a50a4539deb6200013407b001f81dcdb5560639ef030242
data/lib/http/2/client.rb CHANGED
@@ -26,6 +26,7 @@ module HTTP2
26
26
 
27
27
  @local_role = :client
28
28
  @remote_role = :server
29
+ @h2c_upgrade = nil
29
30
 
30
31
  super
31
32
  end
@@ -36,9 +36,14 @@ module HTTP2
36
36
  CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
37
37
 
38
38
  REQUEST_MANDATORY_HEADERS = %w[:scheme :method :authority :path].freeze
39
+
39
40
  RESPONSE_MANDATORY_HEADERS = %w[:status].freeze
40
41
 
41
- EMPTY = [].freeze
42
+ CONNECTION_FRAME_TYPES = %i[settings ping goaway].freeze
43
+
44
+ HEADERS_FRAME_TYPES = %i[headers push_promise].freeze
45
+
46
+ STREAM_OPEN_STATES = %i[open half_closed_local].freeze
42
47
 
43
48
  # Connection encapsulates all of the connection, stream, flow-control,
44
49
  # error management, and other processing logic required for a well-behaved
@@ -47,7 +52,6 @@ module HTTP2
47
52
  # Note that this class should not be used directly. Instead, you want to
48
53
  # use either Client or Server class to drive the HTTP 2.0 exchange.
49
54
  #
50
- # rubocop:disable Metrics/ClassLength
51
55
  class Connection
52
56
  include FlowBuffer
53
57
  include Emitter
@@ -84,7 +88,6 @@ module HTTP2
84
88
  @decompressor = Header::Decompressor.new(settings)
85
89
 
86
90
  @active_stream_count = 0
87
- @last_activated_stream = 0
88
91
  @last_stream_id = 0
89
92
  @streams = {}
90
93
  @streams_recently_closed = {}
@@ -104,6 +107,10 @@ module HTTP2
104
107
  @h2c_upgrade = nil
105
108
  @closed_since = nil
106
109
  @received_frame = false
110
+
111
+ # from mixins
112
+ @listeners = Hash.new { |hash, key| hash[key] = [] }
113
+ @send_buffer = FrameBuffer.new
107
114
  end
108
115
 
109
116
  def closed?
@@ -119,10 +126,10 @@ module HTTP2
119
126
  raise ConnectionClosed if @state == :closed
120
127
  raise StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
121
128
 
122
- connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_activated_stream
129
+ connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_stream_id
123
130
 
124
131
  stream = activate_stream(id: @stream_id, **args)
125
- @last_activated_stream = stream.id
132
+ @last_stream_id = stream.id
126
133
 
127
134
  @stream_id += 2
128
135
 
@@ -149,13 +156,7 @@ module HTTP2
149
156
  # @param error [Symbol]
150
157
  # @param payload [String]
151
158
  def goaway(error = :no_error, payload = nil)
152
- last_stream = if (max = @streams.max)
153
- max.first
154
- else
155
- 0
156
- end
157
-
158
- send(type: :goaway, last_stream: last_stream,
159
+ send(type: :goaway, last_stream: @last_stream_id,
159
160
  error: error, payload: payload)
160
161
  @state = :closed
161
162
  @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -174,7 +175,6 @@ module HTTP2
174
175
  #
175
176
  # @param settings [Array or Hash]
176
177
  def settings(payload)
177
- payload = payload.to_a
178
178
  validate_settings(@local_role, payload)
179
179
  @pending_settings << payload
180
180
  send(type: :settings, stream: 0, payload: payload)
@@ -187,7 +187,7 @@ module HTTP2
187
187
  #
188
188
  # @param data [String] Binary encoded string
189
189
  def receive(data)
190
- @recv_buffer << data
190
+ append_str(@recv_buffer, data)
191
191
 
192
192
  # Upon establishment of a TCP connection and determination that
193
193
  # HTTP/2.0 will be used by both peers, each endpoint MUST send a
@@ -212,14 +212,18 @@ module HTTP2
212
212
  end
213
213
 
214
214
  while (frame = @framer.parse(@recv_buffer))
215
+ # @type var stream_id: Integer
216
+ stream_id = frame[:stream]
217
+ frame_type = frame[:type]
218
+
215
219
  if is_a?(Client) && !@received_frame
216
- connection_error(:protocol_error, msg: "didn't receive settings") if frame[:type] != :settings
220
+ connection_error(:protocol_error, msg: "didn't receive settings") if frame_type != :settings
217
221
  @received_frame = true
218
222
  end
219
223
 
220
224
  # Implementations MUST discard frames
221
225
  # that have unknown or unsupported types.
222
- if frame[:type].nil?
226
+ if frame_type.nil?
223
227
  # However, extension frames that appear in
224
228
  # the middle of a header block (Section 4.3) are not permitted; these
225
229
  # MUST be treated as a connection error (Section 5.4.1) of type
@@ -233,7 +237,7 @@ module HTTP2
233
237
  # Header blocks MUST be transmitted as a contiguous sequence of frames
234
238
  # with no interleaved frames of any other type, or from any other stream.
235
239
  unless @continuation.empty?
236
- connection_error unless frame[:type] == :continuation && frame[:stream] == @continuation.first[:stream]
240
+ connection_error unless frame_type == :continuation && stream_id == @continuation.first[:stream]
237
241
 
238
242
  @continuation << frame
239
243
  unless frame[:flags].include? :end_headers
@@ -252,6 +256,8 @@ module HTTP2
252
256
  payload = @continuation.map { |f| f[:payload] }.join
253
257
 
254
258
  frame = @continuation.shift
259
+ frame_type = frame[:type]
260
+
255
261
  @continuation.clear
256
262
 
257
263
  frame.delete(:length)
@@ -265,14 +271,14 @@ module HTTP2
265
271
  # anything other than 0x0, the endpoint MUST respond with a connection
266
272
  # error (Section 5.4.1) of type PROTOCOL_ERROR.
267
273
  if connection_frame?(frame)
268
- connection_error(:protocol_error) unless frame[:stream].zero?
274
+ connection_error(:protocol_error) unless stream_id.zero?
269
275
  connection_management(frame)
270
276
  else
271
- case frame[:type]
277
+ case frame_type
272
278
  when :headers
273
279
  # When server receives even-numbered stream identifier,
274
280
  # the endpoint MUST respond with a connection error of type PROTOCOL_ERROR.
275
- connection_error if frame[:stream].even? && is_a?(Server)
281
+ connection_error if stream_id.even? && is_a?(Server)
276
282
 
277
283
  # The last frame in a sequence of HEADERS/CONTINUATION
278
284
  # frames MUST have the END_HEADERS flag set.
@@ -289,13 +295,13 @@ module HTTP2
289
295
  decode_headers(frame)
290
296
  return if @state == :closed
291
297
 
292
- stream = @streams[frame[:stream]]
298
+ stream = @streams[stream_id]
293
299
  if stream.nil?
294
300
  verify_pseudo_headers(frame)
295
301
 
296
- verify_stream_order(frame[:stream])
302
+ verify_stream_order(stream_id)
297
303
  stream = activate_stream(
298
- id: frame[:stream],
304
+ id: stream_id,
299
305
  weight: frame[:weight] || DEFAULT_WEIGHT,
300
306
  dependency: frame[:dependency] || 0,
301
307
  exclusive: frame[:exclusive] || false
@@ -326,18 +332,18 @@ module HTTP2
326
332
  # that is not currently in the "idle" state) as a connection error
327
333
  # (Section 5.4.1) of type PROTOCOL_ERROR, unless the receiver
328
334
  # recently sent a RST_STREAM frame to cancel the associated stream.
329
- parent = @streams[frame[:stream]]
335
+ parent = @streams[stream_id]
330
336
  pid = frame[:promise_stream]
331
337
 
332
338
  # if PUSH parent is recently closed, RST_STREAM the push
333
- if @streams_recently_closed[frame[:stream]]
339
+ if @streams_recently_closed[stream_id]
334
340
  send(type: :rst_stream, stream: pid, error: :refused_stream)
335
341
  return
336
342
  end
337
343
 
338
344
  connection_error(msg: "missing parent ID") if parent.nil?
339
345
 
340
- unless parent.state == :open || parent.state == :half_closed_local
346
+ unless STREAM_OPEN_STATES.include?(parent.state)
341
347
  # An endpoint might receive a PUSH_PROMISE frame after it sends
342
348
  # RST_STREAM. PUSH_PROMISE causes a stream to become "reserved".
343
349
  # The RST_STREAM does not cancel any promised stream. Therefore, if
@@ -358,21 +364,21 @@ module HTTP2
358
364
  emit(:promise, stream)
359
365
  stream << frame
360
366
  else
361
- if (stream = @streams[frame[:stream]])
367
+ if (stream = @streams[stream_id])
362
368
  stream << frame
363
- if frame[:type] == :data
369
+ if frame_type == :data
364
370
  update_local_window(frame)
365
371
  calculate_window_update(@local_window_limit)
366
372
  end
367
373
  else
368
- case frame[:type]
374
+ case frame_type
369
375
  # The PRIORITY frame can be sent for a stream in the "idle" or
370
376
  # "closed" state. This allows for the reprioritization of a
371
377
  # group of dependent streams by altering the priority of an
372
378
  # unused or closed parent stream.
373
379
  when :priority
374
380
  stream = activate_stream(
375
- id: frame[:stream],
381
+ id: stream_id,
376
382
  weight: frame[:weight] || DEFAULT_WEIGHT,
377
383
  dependency: frame[:dependency] || 0,
378
384
  exclusive: frame[:exclusive] || false
@@ -387,16 +393,19 @@ module HTTP2
387
393
  # "closed" stream. A receiver MUST NOT treat this as an error
388
394
  # (see Section 5.1).
389
395
  when :window_update
390
- stream = @streams_recently_closed[frame[:stream]]
391
- connection_error(:protocol_error, msg: "sent window update on idle stream") unless stream
396
+ unless @streams_recently_closed.key?(stream_id)
397
+ connection_error(:protocol_error, msg: "sent window update on idle stream")
398
+ end
399
+ stream = @streams_recently_closed[stream_id]
392
400
  process_window_update(frame: frame, encode: true)
393
401
  # Endpoints MUST ignore
394
402
  # WINDOW_UPDATE or RST_STREAM frames received in this state (closed), though
395
403
  # endpoints MAY choose to treat frames that arrive a significant
396
404
  # time after sending END_STREAM as a connection error.
397
405
  when :rst_stream
398
- stream = @streams_recently_closed[frame[:stream]]
399
- connection_error(:protocol_error, msg: "sent window update on idle stream") unless stream
406
+ unless @streams_recently_closed.key?(stream_id)
407
+ connection_error(:protocol_error, msg: "sent window update on idle stream")
408
+ end
400
409
  else
401
410
  # An endpoint that receives an unexpected stream identifier
402
411
  # MUST respond with a connection error of type PROTOCOL_ERROR.
@@ -425,35 +434,33 @@ module HTTP2
425
434
  # @note all frames are currently delivered in FIFO order.
426
435
  # @param frame [Hash]
427
436
  def send(frame)
437
+ frame_type = frame[:type]
438
+
428
439
  emit(:frame_sent, frame)
429
- if frame[:type] == :data
440
+ if frame_type == :data
430
441
  send_data(frame, true)
431
442
 
432
- elsif frame[:type] == :rst_stream && frame[:error] == :protocol_error
443
+ elsif frame_type == :rst_stream && frame[:error] == :protocol_error
433
444
  # An endpoint can end a connection at any time. In particular, an
434
445
  # endpoint MAY choose to treat a stream error as a connection error.
435
446
 
436
- goaway(frame[:error])
447
+ goaway(:protocol_error)
437
448
  else
438
449
  # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
439
450
  # RST_STREAM that are not protocol errors
440
- frames = encode(frame)
441
- frames.each { |f| emit(:frame, f) }
451
+ encode(frame)
442
452
  end
443
453
  end
444
454
 
445
455
  # Applies HTTP 2.0 binary encoding to the frame.
446
456
  #
447
457
  # @param frame [Hash]
448
- # @return [Array of Buffer] encoded frame
449
458
  def encode(frame)
450
- frames = if frame[:type] == :headers || frame[:type] == :push_promise
451
- encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
452
- else
453
- [frame] # otherwise one frame
454
- end
455
-
456
- frames.map { |f| @framer.generate(f) }
459
+ if HEADERS_FRAME_TYPES.include?(frame[:type])
460
+ encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
461
+ else
462
+ emit(:frame, @framer.generate(frame))
463
+ end
457
464
  end
458
465
 
459
466
  # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
@@ -462,10 +469,7 @@ module HTTP2
462
469
  # @param frame [Hash]
463
470
  # @return [Boolean]
464
471
  def connection_frame?(frame)
465
- (frame[:stream]).zero? ||
466
- frame[:type] == :settings ||
467
- frame[:type] == :ping ||
468
- frame[:type] == :goaway
472
+ frame[:stream].zero? || CONNECTION_FRAME_TYPES.include?(frame[:type])
469
473
  end
470
474
 
471
475
  # Process received connection frame (stream ID = 0).
@@ -476,14 +480,18 @@ module HTTP2
476
480
  #
477
481
  # @param frame [Hash]
478
482
  def connection_management(frame)
483
+ frame_type = frame[:type]
484
+
479
485
  case @state
480
486
  when :waiting_connection_preface
481
487
  # The first frame MUST be a SETTINGS frame at the start of a connection.
488
+ connection_error unless frame[:type] == :settings
489
+
482
490
  @state = :connected
483
491
  connection_settings(frame)
484
492
 
485
493
  when :connected
486
- case frame[:type]
494
+ case frame_type
487
495
  when :settings
488
496
  connection_settings(frame)
489
497
  when :window_update
@@ -498,23 +506,24 @@ module HTTP2
498
506
  @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
499
507
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
500
508
  when :altsvc
509
+ origin = frame[:origin]
501
510
  # 4. The ALTSVC HTTP/2 Frame
502
511
  # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
503
512
  # information is invalid and MUST be ignored.
504
- emit(frame[:type], frame) if frame[:origin] && !frame[:origin].empty?
513
+ emit(:altsvc, frame) if origin && !origin.empty?
505
514
  when :origin
506
515
  return if @h2c_upgrade || !frame[:flags].empty?
507
516
 
508
- frame[:payload].each do |origin|
509
- emit(frame[:type], origin)
517
+ frame[:payload].each do |orig|
518
+ emit(:origin, orig)
510
519
  end
511
520
  when :blocked
512
- emit(frame[:type], frame)
521
+ emit(:blocked, frame)
513
522
  else
514
523
  connection_error
515
524
  end
516
525
  when :closed
517
- case frame[:type]
526
+ case frame_type
518
527
  when :goaway
519
528
  connection_error
520
529
  when :ping
@@ -573,7 +582,7 @@ module HTTP2
573
582
  # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
574
583
  # Values outside this range MUST be treated as a connection error
575
584
  # (Section 5.4.1) of type PROTOCOL_ERROR.
576
- next if v >= 16_384 && v <= 16_777_215
585
+ next if v.between?(16_384, 16_777_215)
577
586
 
578
587
  connection_error(:protocol_error, msg: "invalid #{key} value")
579
588
  # when :settings_max_concurrent_streams
@@ -591,19 +600,19 @@ module HTTP2
591
600
  #
592
601
  # @param frame [Hash]
593
602
  def connection_settings(frame)
594
- connection_error unless frame[:type] == :settings && (frame[:stream]).zero?
595
-
596
603
  # Apply settings.
597
604
  # side =
598
605
  # local: previously sent and pended our settings should be effective
599
606
  # remote: just received peer settings should immediately be effective
600
- settings, side = if frame[:flags].include?(:ack)
601
- # Process pending settings we have sent.
602
- [@pending_settings.shift, :local]
603
- else
604
- validate_settings(@remote_role, frame[:payload])
605
- [frame[:payload], :remote]
606
- end
607
+ if frame[:flags].include?(:ack)
608
+ # Process pending settings we have sent.
609
+ settings = @pending_settings.shift
610
+ side = :local
611
+ else
612
+ validate_settings(@remote_role, frame[:payload])
613
+ settings = frame[:payload]
614
+ side = :remote
615
+ end
607
616
 
608
617
  settings.each do |key, v|
609
618
  case side
@@ -699,40 +708,48 @@ module HTTP2
699
708
 
700
709
  # Encode headers payload and update connection compressor state.
701
710
  #
702
- # @param frame [Hash]
703
- # @return [Array of Frame]
704
- def encode_headers(frame)
705
- payload = frame[:payload]
711
+ # @param headers_frame [Hash]
712
+ def encode_headers(headers_frame)
713
+ payload = headers_frame[:payload]
706
714
  begin
707
- payload = frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
715
+ payload = headers_frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
708
716
  rescue StandardError => e
709
717
  connection_error(:compression_error, e: e)
710
718
  end
711
719
 
720
+ max_frame_size = @remote_settings[:settings_max_frame_size]
721
+
712
722
  # if single frame, return immediately
713
- return [frame] if payload.bytesize <= @remote_settings[:settings_max_frame_size]
723
+ if payload.bytesize <= max_frame_size
724
+ emit(:frame, @framer.generate(headers_frame))
725
+ return
726
+ end
714
727
 
715
- frames = []
728
+ # split into multiple CONTINUATION frames
729
+ headers_frame[:flags].delete(:end_headers)
730
+ headers_frame[:payload] = payload.byteslice(0, max_frame_size)
731
+ payload = payload.byteslice(max_frame_size..-1)
716
732
 
717
- until payload.nil? || payload.empty?
718
- cont = frame.dup
733
+ # emit first HEADERS frame
734
+ emit(:frame, @framer.generate(headers_frame))
719
735
 
720
- # first frame remains HEADERS
721
- unless frames.empty?
722
- cont[:type] = :continuation
723
- cont[:flags] = EMPTY
724
- end
736
+ loop do
737
+ continuation_frame = headers_frame.merge(
738
+ type: :continuation,
739
+ flags: EMPTY,
740
+ payload: payload.byteslice(0, max_frame_size)
741
+ )
725
742
 
726
- cont[:payload] = payload.byteslice(0, @remote_settings[:settings_max_frame_size])
727
- payload = payload.byteslice(@remote_settings[:settings_max_frame_size]..-1)
743
+ payload = payload.byteslice(max_frame_size..-1)
728
744
 
729
- frames << cont
730
- end
731
-
732
- frames.first[:flags].delete(:end_headers)
733
- frames.last[:flags] = [:end_headers]
745
+ if payload.nil? || payload.empty?
746
+ continuation_frame[:flags] = [:end_headers]
747
+ emit(:frame, @framer.generate(continuation_frame))
748
+ break
749
+ end
734
750
 
735
- frames
751
+ emit(:frame, @framer.generate(continuation_frame))
752
+ end
736
753
  end
737
754
 
738
755
  # Activates new incoming or outgoing stream and registers appropriate
@@ -758,17 +775,22 @@ module HTTP2
758
775
  # is closed, with a minimum of 15s RTT time window.
759
776
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
760
777
 
761
- # TODO: use a drop_while! variant whenever there is one.
762
- @streams_recently_closed = @streams_recently_closed.drop_while do |_, v|
763
- (now - v) > 15
764
- end.to_h
778
+ _, closed_since = @streams_recently_closed.first
779
+
780
+ # forego recently closed recycling if empty or the first element
781
+ # hasn't expired yet (it's ordered).
782
+ if closed_since && (now - closed_since) > 15
783
+ # discards all streams which have closed for a while.
784
+ # TODO: use a drop_while! variant whenever there is one.
785
+ @streams_recently_closed = @streams_recently_closed.drop_while do |_, since|
786
+ (now - since) > 15
787
+ end.to_h
788
+ end
765
789
 
766
790
  @streams_recently_closed[id] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
767
791
  end
768
792
 
769
- stream.on(:promise, &method(:promise)) if is_a? Server
770
- stream.on(:frame, &method(:send))
771
-
793
+ stream.on(:frame, &method(:send))
772
794
  @streams[id] = stream
773
795
  end
774
796
 
@@ -820,5 +842,4 @@ module HTTP2
820
842
  yield
821
843
  end
822
844
  end
823
- # rubocop:enable Metrics/ClassLength
824
845
  end
@@ -12,7 +12,7 @@ module HTTP2
12
12
  def on(event, &block)
13
13
  raise ArgumentError, "must provide callback" unless block
14
14
 
15
- listeners(event.to_sym).push block
15
+ @listeners[event] << block
16
16
  end
17
17
 
18
18
  # Subscribe to next event (at most once) for specified type.
@@ -32,16 +32,9 @@ module HTTP2
32
32
  # @param args [Array] arguments to be passed to the callbacks
33
33
  # @param block [Proc] callback function
34
34
  def emit(event, *args, &block)
35
- listeners(event).delete_if do |cb|
35
+ @listeners[event].delete_if do |cb|
36
36
  :delete == cb.call(*args, &block) # rubocop:disable Style/YodaCondition
37
37
  end
38
38
  end
39
-
40
- private
41
-
42
- def listeners(event)
43
- @listeners ||= Hash.new { |hash, key| hash[key] = [] }
44
- @listeners[event]
45
- end
46
39
  end
47
40
  end
@@ -2,6 +2,26 @@
2
2
 
3
3
  module HTTP2
4
4
  module BufferUtils
5
+ if RUBY_VERSION > "3.4.0"
6
+ def append_str(str, data)
7
+ str.append_as_bytes(data)
8
+ end
9
+ else
10
+ def append_str(str, data)
11
+ enc = data.encoding
12
+ reset = false
13
+
14
+ if enc != Encoding::BINARY
15
+ reset = true
16
+ data = data.dup if data.frozen?
17
+ data.force_encoding(Encoding::BINARY)
18
+ end
19
+ str << data
20
+ ensure
21
+ data.force_encoding(enc) if reset
22
+ end
23
+ end
24
+
5
25
  def read_str(str, n)
6
26
  return "".b if n == 0
7
27
 
@@ -28,7 +48,7 @@ module HTTP2
28
48
  packed_str = array_to_pack.pack(template)
29
49
  case offset
30
50
  when -1
31
- buffer << packed_str
51
+ append_str(buffer, packed_str)
32
52
  when 0
33
53
  buffer.prepend(packed_str)
34
54
  else