quicsilver 0.5.0-x86_64-linux
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 +7 -0
- data/.github/workflows/ci.yml +49 -0
- data/.github/workflows/cibuildgem.yaml +117 -0
- data/.gitignore +21 -0
- data/.gitmodules +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +175 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +21 -0
- data/README.md +261 -0
- data/Rakefile +130 -0
- data/benchmarks/baseline.rb +175 -0
- data/benchmarks/compare.rb +398 -0
- data/benchmarks/components.rb +191 -0
- data/benchmarks/helpers.rb +162 -0
- data/benchmarks/latency.rb +128 -0
- data/benchmarks/loss.rb +193 -0
- data/benchmarks/mobile.rb +125 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/connection_identity.md +141 -0
- data/examples/README.md +58 -0
- data/examples/benchmark.rb +191 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +69 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/latency_aware_routing.rb +382 -0
- data/examples/lifecycle_drain.rb +264 -0
- data/examples/minimal_http3_server.rb +14 -0
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +42 -0
- data/examples/rails_feature_test.rb +280 -0
- data/examples/server_stats.rb +117 -0
- data/examples/simple_client_test.rb +26 -0
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/examples/webtransport_smoke.rb +546 -0
- data/examples/worker_load_balancing.rb +425 -0
- data/ext/quicsilver/extconf.rb +124 -0
- data/ext/quicsilver/include/msquic.h +2226 -0
- data/ext/quicsilver/include/msquic_posix.h +696 -0
- data/ext/quicsilver/include/quic_sal_stub.h +314 -0
- data/ext/quicsilver/msquic_patch.rb +68 -0
- data/ext/quicsilver/patches/reliable-reset.patch +723 -0
- data/ext/quicsilver/patches/stream-final-size.patch +47 -0
- data/ext/quicsilver/quicsilver.c +2741 -0
- data/lib/quicsilver/3.4/quicsilver.so +0 -0
- data/lib/quicsilver/4.0/quicsilver.so +0 -0
- data/lib/quicsilver/client/client.rb +708 -0
- data/lib/quicsilver/client/connection_pool.rb +202 -0
- data/lib/quicsilver/client/request.rb +190 -0
- data/lib/quicsilver/errors.rb +39 -0
- data/lib/quicsilver/libmsquic.so.2 +0 -0
- data/lib/quicsilver/protocol/adapter.rb +196 -0
- data/lib/quicsilver/protocol/capsule.rb +38 -0
- data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
- data/lib/quicsilver/protocol/datagram.rb +40 -0
- data/lib/quicsilver/protocol/frame_parser.rb +141 -0
- data/lib/quicsilver/protocol/frame_reader.rb +84 -0
- data/lib/quicsilver/protocol/frames.rb +454 -0
- data/lib/quicsilver/protocol/priority.rb +65 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +50 -0
- data/lib/quicsilver/protocol/request_parser.rb +289 -0
- data/lib/quicsilver/protocol/response_encoder.rb +107 -0
- data/lib/quicsilver/protocol/response_parser.rb +147 -0
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
- data/lib/quicsilver/protocol/web_transport.rb +214 -0
- data/lib/quicsilver/rack/context.rb +33 -0
- data/lib/quicsilver/response.rb +40 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/rack_adapter.rb +77 -0
- data/lib/quicsilver/server/receive_queue.rb +106 -0
- data/lib/quicsilver/server/request_handler.rb +149 -0
- data/lib/quicsilver/server/request_registry.rb +63 -0
- data/lib/quicsilver/server/scheduler.rb +50 -0
- data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
- data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
- data/lib/quicsilver/server/server.rb +1080 -0
- data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
- data/lib/quicsilver/server/web_transport_manager.rb +281 -0
- data/lib/quicsilver/server/web_transport_registry.rb +40 -0
- data/lib/quicsilver/server/web_transport_session.rb +812 -0
- data/lib/quicsilver/server/web_transport_stream.rb +326 -0
- data/lib/quicsilver/transport/configuration.rb +234 -0
- data/lib/quicsilver/transport/connection.rb +594 -0
- data/lib/quicsilver/transport/connection_stats.rb +53 -0
- data/lib/quicsilver/transport/event_loop.rb +74 -0
- data/lib/quicsilver/transport/inbound_stream.rb +53 -0
- data/lib/quicsilver/transport/stream.rb +43 -0
- data/lib/quicsilver/transport/stream_event.rb +49 -0
- data/lib/quicsilver/transport/stream_id.rb +26 -0
- data/lib/quicsilver/version.rb +3 -0
- data/lib/quicsilver.rb +96 -0
- data/lib/rackup/handler/quicsilver.rb +96 -0
- data/quicsilver.gemspec +66 -0
- metadata +362 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "huffman"
|
|
3
|
+
|
|
4
|
+
module Quicsilver
|
|
5
|
+
module Protocol
|
|
6
|
+
module Qpack
|
|
7
|
+
class Encoder
|
|
8
|
+
STATIC_TABLE = Protocol::STATIC_TABLE
|
|
9
|
+
|
|
10
|
+
# Pre-built hash for O(1) static table lookups
|
|
11
|
+
STATIC_LOOKUP_FULL = {} # "name\0value" => index
|
|
12
|
+
STATIC_LOOKUP_NAME = {} # name => first_index
|
|
13
|
+
|
|
14
|
+
STATIC_TABLE.each_with_index do |(tbl_name, tbl_value), idx|
|
|
15
|
+
STATIC_LOOKUP_FULL["#{tbl_name}\0#{tbl_value}".freeze] = idx
|
|
16
|
+
STATIC_LOOKUP_NAME[tbl_name] ||= idx
|
|
17
|
+
end
|
|
18
|
+
STATIC_LOOKUP_FULL.freeze
|
|
19
|
+
STATIC_LOOKUP_NAME.freeze
|
|
20
|
+
|
|
21
|
+
PREFIX = "\x00\x00".b.freeze
|
|
22
|
+
|
|
23
|
+
FIELD_CACHE_MAX = 512
|
|
24
|
+
|
|
25
|
+
def initialize(huffman: true)
|
|
26
|
+
@huffman = huffman
|
|
27
|
+
@field_cache = {}
|
|
28
|
+
@block_cache = {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Caches are keyed by header content, never by identity or object_id:
|
|
32
|
+
# callers may reuse and mutate the same array, and an identity hit
|
|
33
|
+
# would return the previous encoding.
|
|
34
|
+
#
|
|
35
|
+
# Caching a whole field block is only sound because encoding is
|
|
36
|
+
# static-table-only: Required Insert Count is always zero, so the bytes
|
|
37
|
+
# never depend on connection state. Implementing the dynamic table
|
|
38
|
+
# would invalidate @block_cache, since indices are relative to a Base.
|
|
39
|
+
def encode(headers)
|
|
40
|
+
if headers.is_a?(Array) && headers.size <= 16
|
|
41
|
+
# Content-based caching for small header sets
|
|
42
|
+
block_key = headers.map { |n, v| "#{n}\0#{v}" }.join("\x01")
|
|
43
|
+
cached_block = @block_cache[block_key]
|
|
44
|
+
return cached_block if cached_block
|
|
45
|
+
|
|
46
|
+
result_frozen = encode_fields(headers).freeze
|
|
47
|
+
if @block_cache.size < BLOCK_CACHE_MAX
|
|
48
|
+
@block_cache[block_key.freeze] = result_frozen
|
|
49
|
+
end
|
|
50
|
+
return result_frozen
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
encode_fields(headers)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
BLOCK_CACHE_MAX = 128
|
|
57
|
+
|
|
58
|
+
private def encode_fields(headers)
|
|
59
|
+
out = encode_prefix
|
|
60
|
+
headers.each do |name, value|
|
|
61
|
+
name = name.to_s
|
|
62
|
+
value = value.to_s
|
|
63
|
+
# Downcase only if needed (most HTTP/3 headers are already lowercase)
|
|
64
|
+
name = name.downcase if name.match?(/[A-Z]/)
|
|
65
|
+
|
|
66
|
+
cache_key = "#{name}\0#{value}"
|
|
67
|
+
|
|
68
|
+
# Check field cache
|
|
69
|
+
cached = @field_cache[cache_key]
|
|
70
|
+
if cached
|
|
71
|
+
out << cached
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
field_start = out.bytesize
|
|
76
|
+
|
|
77
|
+
full_idx = STATIC_LOOKUP_FULL[cache_key]
|
|
78
|
+
|
|
79
|
+
if full_idx
|
|
80
|
+
# Indexed Field Line
|
|
81
|
+
out << encode_prefixed_int(full_idx, 6, 0xC0)
|
|
82
|
+
else
|
|
83
|
+
name_idx = STATIC_LOOKUP_NAME[name]
|
|
84
|
+
if name_idx
|
|
85
|
+
# Literal with Name Reference
|
|
86
|
+
out << encode_prefixed_int(name_idx, 4, 0x50)
|
|
87
|
+
encode_str_into(out, value)
|
|
88
|
+
else
|
|
89
|
+
# Literal with Literal Name
|
|
90
|
+
encode_literal_into(out, name, value)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Cache the encoded field bytes
|
|
95
|
+
if @field_cache.size < FIELD_CACHE_MAX
|
|
96
|
+
@field_cache[cache_key.freeze] = out.byteslice(field_start..).freeze
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
out
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def lookup(name, value)
|
|
103
|
+
name = name.to_s.downcase
|
|
104
|
+
value = value.to_s
|
|
105
|
+
full_idx = STATIC_LOOKUP_FULL["#{name}\0#{value}"]
|
|
106
|
+
return [full_idx, true] if full_idx
|
|
107
|
+
name_idx = STATIC_LOOKUP_NAME[name]
|
|
108
|
+
name_idx ? [name_idx, false] : nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def encode_prefix
|
|
112
|
+
PREFIX.dup
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Public API for encoding a single string value (used by tests/external code)
|
|
116
|
+
def encode_str(value)
|
|
117
|
+
out = "".b
|
|
118
|
+
encode_str_into(out, value.to_s)
|
|
119
|
+
out
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# Pattern 1: Indexed Field Line (1xxxxxxx) — kept for test compatibility
|
|
125
|
+
def encode_indexed(index)
|
|
126
|
+
encode_prefixed_int(index, 6, 0xC0)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def encode_literal_into(out, name, value)
|
|
130
|
+
name_b = name.b
|
|
131
|
+
if @huffman
|
|
132
|
+
huffman_name = Huffman.encode(name_b)
|
|
133
|
+
if huffman_name.bytesize < name_b.bytesize
|
|
134
|
+
out << encode_prefixed_int(huffman_name.bytesize, 3, 0x28)
|
|
135
|
+
out << huffman_name
|
|
136
|
+
encode_str_into(out, value)
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
out << encode_prefixed_int(name_b.bytesize, 3, 0x20)
|
|
141
|
+
out << name_b
|
|
142
|
+
encode_str_into(out, value)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def encode_str_into(out, value)
|
|
146
|
+
value_b = value.b
|
|
147
|
+
if @huffman
|
|
148
|
+
huffman = Huffman.encode(value_b)
|
|
149
|
+
if huffman.bytesize < value_b.bytesize
|
|
150
|
+
out << encode_prefixed_int(huffman.bytesize, 7, 0x80)
|
|
151
|
+
out << huffman
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
out << encode_prefixed_int(value_b.bytesize, 7, 0x00)
|
|
156
|
+
out << value_b
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Pre-computed prefix integer tables for hot encode paths.
|
|
160
|
+
# Key: [prefix_bits, pattern] => Array of frozen encoded strings indexed by value.
|
|
161
|
+
# Covers all values up to max_prefix (single-byte) and a range beyond.
|
|
162
|
+
PREFIXED_INT_CACHE = {}
|
|
163
|
+
[
|
|
164
|
+
[6, 0xC0], # Indexed Field Line
|
|
165
|
+
[4, 0x50], # Literal with Name Reference (static)
|
|
166
|
+
[7, 0x80], # Huffman string length
|
|
167
|
+
[7, 0x00], # Raw string length
|
|
168
|
+
[3, 0x28], # Huffman literal name length
|
|
169
|
+
[3, 0x20], # Raw literal name length
|
|
170
|
+
].each do |prefix_bits, pattern|
|
|
171
|
+
max_prefix = (1 << prefix_bits) - 1
|
|
172
|
+
# Pre-compute single-byte range + a bit beyond for multi-byte
|
|
173
|
+
limit = [max_prefix + 64, 256].min
|
|
174
|
+
table = Array.new(limit) do |value|
|
|
175
|
+
if value < max_prefix
|
|
176
|
+
(pattern | value).chr(Encoding::BINARY).freeze
|
|
177
|
+
else
|
|
178
|
+
buf = (pattern | max_prefix).chr(Encoding::BINARY)
|
|
179
|
+
v = value - max_prefix
|
|
180
|
+
while v >= 128
|
|
181
|
+
buf << ((v & 0x7F) | 0x80).chr(Encoding::BINARY)
|
|
182
|
+
v >>= 7
|
|
183
|
+
end
|
|
184
|
+
buf << v.chr(Encoding::BINARY)
|
|
185
|
+
buf.freeze
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
PREFIXED_INT_CACHE[[prefix_bits, pattern]] = table.freeze
|
|
189
|
+
end
|
|
190
|
+
PREFIXED_INT_CACHE.freeze
|
|
191
|
+
|
|
192
|
+
# RFC 7541 prefix integer encoding
|
|
193
|
+
def encode_prefixed_int(value, prefix_bits, pattern)
|
|
194
|
+
table = PREFIXED_INT_CACHE[[prefix_bits, pattern]]
|
|
195
|
+
if table && value < table.size
|
|
196
|
+
return table[value]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
max_prefix = (1 << prefix_bits) - 1
|
|
200
|
+
|
|
201
|
+
if value < max_prefix
|
|
202
|
+
(pattern | value).chr(Encoding::BINARY)
|
|
203
|
+
else
|
|
204
|
+
buf = (pattern | max_prefix).chr(Encoding::BINARY)
|
|
205
|
+
value -= max_prefix
|
|
206
|
+
while value >= 128
|
|
207
|
+
buf << ((value & 0x7F) | 0x80).chr(Encoding::BINARY)
|
|
208
|
+
value >>= 7
|
|
209
|
+
end
|
|
210
|
+
buf << value.chr(Encoding::BINARY)
|
|
211
|
+
buf
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "decoder"
|
|
4
|
+
require_relative "huffman"
|
|
5
|
+
|
|
6
|
+
module Quicsilver
|
|
7
|
+
module Protocol
|
|
8
|
+
module Qpack
|
|
9
|
+
# Decodes a QPACK header block into [name, value] pairs.
|
|
10
|
+
#
|
|
11
|
+
# Default implementation uses the static table only (no dynamic table).
|
|
12
|
+
# To add dynamic table support, implement a class with the same #decode interface:
|
|
13
|
+
#
|
|
14
|
+
# class MyDynamicDecoder
|
|
15
|
+
# def decode(payload)
|
|
16
|
+
# # parse QPACK field lines from payload
|
|
17
|
+
# # yield [name, value] for each decoded header
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Then inject it:
|
|
22
|
+
# RequestParser.new(data, decoder: MyDynamicDecoder.new)
|
|
23
|
+
# ResponseParser.new(data, decoder: MyDynamicDecoder.new)
|
|
24
|
+
#
|
|
25
|
+
class HeaderBlockDecoder
|
|
26
|
+
include Decoder
|
|
27
|
+
|
|
28
|
+
DECODE_CACHE_MAX = 256
|
|
29
|
+
|
|
30
|
+
# Shared default instance for parsers that don't need custom decoders
|
|
31
|
+
def self.default
|
|
32
|
+
@default ||= new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@decode_cache = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Decode a QPACK header block payload (RFC 9204 §4.5).
|
|
40
|
+
# Yields [name, value] for each decoded field line.
|
|
41
|
+
def decode(payload)
|
|
42
|
+
return if payload.nil? || payload.bytesize < 2
|
|
43
|
+
|
|
44
|
+
# Check cache for previously decoded payloads
|
|
45
|
+
if payload.bytesize <= 256
|
|
46
|
+
cached = @decode_cache[payload]
|
|
47
|
+
if cached
|
|
48
|
+
cached.each { |name, value| yield name, value }
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
headers = []
|
|
54
|
+
|
|
55
|
+
# Decode Required Insert Count (RFC 9204 §4.5.1) — 8-bit prefix integer
|
|
56
|
+
required_insert_count, ric_bytes = decode_prefix_integer_str(payload, 0, 8, 0x00)
|
|
57
|
+
offset = ric_bytes
|
|
58
|
+
|
|
59
|
+
# Decode Delta Base (RFC 9204 §4.5.1) — 7-bit prefix integer with sign bit
|
|
60
|
+
delta_base, db_bytes = decode_prefix_integer_str(payload, offset, 7, 0x80)
|
|
61
|
+
offset += db_bytes
|
|
62
|
+
|
|
63
|
+
# Static-only mode: Required Insert Count and Delta Base must be 0
|
|
64
|
+
if required_insert_count != 0 || delta_base != 0
|
|
65
|
+
raise Protocol::FrameError.new(
|
|
66
|
+
"Dynamic QPACK table not supported (required_insert_count=#{required_insert_count}, delta_base=#{delta_base})",
|
|
67
|
+
error_code: Protocol::QPACK_DECOMPRESSION_FAILED
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
while offset < payload.bytesize
|
|
72
|
+
byte = payload.getbyte(offset)
|
|
73
|
+
|
|
74
|
+
# Indexed Field Line (1Txxxxxx) — name + value from static table
|
|
75
|
+
if (byte & 0x80) == 0x80
|
|
76
|
+
index, bytes_consumed = decode_prefix_integer_str(payload, offset, 6, 0xC0)
|
|
77
|
+
offset += bytes_consumed
|
|
78
|
+
|
|
79
|
+
if index < Protocol::STATIC_TABLE.size
|
|
80
|
+
name, value = Protocol::STATIC_TABLE[index]
|
|
81
|
+
headers << [name, value]
|
|
82
|
+
yield name, value
|
|
83
|
+
else
|
|
84
|
+
raise Protocol::FrameError.new(
|
|
85
|
+
"Invalid QPACK static table index #{index}",
|
|
86
|
+
error_code: Protocol::QPACK_DECOMPRESSION_FAILED
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Literal with Name Reference (01NTxxxx) — name from static table, literal value
|
|
91
|
+
elsif (byte & 0xC0) == 0x40
|
|
92
|
+
index, bytes_consumed = decode_prefix_integer_str(payload, offset, 4, 0xF0)
|
|
93
|
+
offset += bytes_consumed
|
|
94
|
+
|
|
95
|
+
if index >= Protocol::STATIC_TABLE.size
|
|
96
|
+
raise Protocol::FrameError.new(
|
|
97
|
+
"Invalid QPACK static table index #{index}",
|
|
98
|
+
error_code: Protocol::QPACK_DECOMPRESSION_FAILED
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
name = Protocol::STATIC_TABLE[index][0]
|
|
103
|
+
value, consumed = decode_qpack_string_from_str(payload, offset)
|
|
104
|
+
offset += consumed
|
|
105
|
+
headers << [name, value]
|
|
106
|
+
yield name, value
|
|
107
|
+
|
|
108
|
+
# Literal with Literal Name (001NHxxx) — both name and value are literals
|
|
109
|
+
elsif (byte & 0xE0) == 0x20
|
|
110
|
+
huffman_name = (byte & 0x08) != 0
|
|
111
|
+
name_len, name_len_bytes = decode_prefix_integer_str(payload, offset, 3, 0x28)
|
|
112
|
+
offset += name_len_bytes
|
|
113
|
+
raw_name = payload.byteslice(offset, name_len)
|
|
114
|
+
name = if huffman_name
|
|
115
|
+
Huffman.decode(raw_name) || raw_name
|
|
116
|
+
else
|
|
117
|
+
raw_name
|
|
118
|
+
end
|
|
119
|
+
offset += name_len
|
|
120
|
+
|
|
121
|
+
value, consumed = decode_qpack_string_from_str(payload, offset)
|
|
122
|
+
offset += consumed
|
|
123
|
+
|
|
124
|
+
headers << [name, value]
|
|
125
|
+
yield name, value
|
|
126
|
+
else
|
|
127
|
+
break
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Cache the result
|
|
132
|
+
if payload.bytesize <= 256 && @decode_cache.size < DECODE_CACHE_MAX
|
|
133
|
+
key = payload.frozen? ? payload : payload.dup.freeze
|
|
134
|
+
@decode_cache[key] = headers.freeze
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|