tipi 0.37 → 0.40
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 +4 -4
- data/.github/workflows/test.yml +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +53 -29
- data/Gemfile.lock +23 -5
- data/TODO.md +79 -5
- data/df/sample_agent.rb +1 -1
- data/df/server.rb +63 -6
- data/examples/automatic_certificate.rb +193 -0
- data/examples/http_server.rb +11 -3
- data/examples/http_server_forked.rb +5 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +38 -0
- data/examples/http_server_throttled.rb +3 -2
- data/examples/https_server.rb +10 -1
- data/examples/https_wss_server.rb +2 -1
- data/examples/rack_server.rb +5 -0
- data/examples/rack_server_https.rb +1 -1
- data/examples/rack_server_https_forked.rb +4 -3
- data/examples/routing_server.rb +5 -4
- data/examples/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/lib/tipi.rb +6 -0
- data/lib/tipi/digital_fabric/agent.rb +16 -13
- data/lib/tipi/digital_fabric/agent_proxy.rb +79 -27
- data/lib/tipi/digital_fabric/protocol.rb +71 -14
- data/lib/tipi/digital_fabric/request_adapter.rb +7 -7
- data/lib/tipi/digital_fabric/service.rb +10 -8
- data/lib/tipi/http1_adapter.rb +87 -36
- data/lib/tipi/http2_adapter.rb +37 -4
- data/lib/tipi/http2_stream.rb +79 -22
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- data/test/test_http_server.rb +22 -37
- data/test/test_request.rb +4 -4
- data/tipi.gemspec +3 -2
- metadata +24 -6
data/lib/tipi/http2_adapter.rb
CHANGED
|
@@ -16,7 +16,9 @@ module Tipi
|
|
|
16
16
|
@opts = opts
|
|
17
17
|
@upgrade_headers = upgrade_headers
|
|
18
18
|
@first = true
|
|
19
|
-
|
|
19
|
+
@rx = (upgrade_headers && upgrade_headers[':rx']) || 0
|
|
20
|
+
@tx = (upgrade_headers && upgrade_headers[':tx']) || 0
|
|
21
|
+
|
|
20
22
|
@interface = ::HTTP2::Server.new
|
|
21
23
|
@connection_fiber = Fiber.current
|
|
22
24
|
@interface.on(:frame, &method(:send_frame))
|
|
@@ -24,6 +26,9 @@ module Tipi
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def send_frame(data)
|
|
29
|
+
if @transfer_count_request
|
|
30
|
+
@transfer_count_request.tx_incr(data.bytesize)
|
|
31
|
+
end
|
|
27
32
|
@conn << data
|
|
28
33
|
rescue Exception => e
|
|
29
34
|
@connection_fiber.transfer e
|
|
@@ -38,6 +43,7 @@ module Tipi
|
|
|
38
43
|
|
|
39
44
|
def upgrade
|
|
40
45
|
@conn << UPGRADE_MESSAGE
|
|
46
|
+
@tx += UPGRADE_MESSAGE.bytesize
|
|
41
47
|
settings = @upgrade_headers['http2-settings']
|
|
42
48
|
Fiber.current.schedule(nil)
|
|
43
49
|
@interface.upgrade(settings, @upgrade_headers, '')
|
|
@@ -49,16 +55,31 @@ module Tipi
|
|
|
49
55
|
def each(&block)
|
|
50
56
|
@interface.on(:stream) { |stream| start_stream(stream, &block) }
|
|
51
57
|
upgrade if @upgrade_headers
|
|
52
|
-
|
|
53
|
-
@conn.recv_loop
|
|
58
|
+
|
|
59
|
+
@conn.recv_loop do |data|
|
|
60
|
+
@rx += data.bytesize
|
|
61
|
+
@interface << data
|
|
62
|
+
end
|
|
54
63
|
rescue SystemCallError, IOError
|
|
55
64
|
# ignore
|
|
56
65
|
ensure
|
|
57
66
|
finalize_client_loop
|
|
58
67
|
end
|
|
68
|
+
|
|
69
|
+
def get_rx_count
|
|
70
|
+
count = @rx
|
|
71
|
+
@rx = 0
|
|
72
|
+
count
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get_tx_count
|
|
76
|
+
count = @tx
|
|
77
|
+
@tx = 0
|
|
78
|
+
count
|
|
79
|
+
end
|
|
59
80
|
|
|
60
81
|
def start_stream(stream, &block)
|
|
61
|
-
stream = HTTP2StreamHandler.new(stream, @conn, @first, &block)
|
|
82
|
+
stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
|
|
62
83
|
@first = nil if @first
|
|
63
84
|
@streams[stream] = true
|
|
64
85
|
end
|
|
@@ -66,11 +87,23 @@ module Tipi
|
|
|
66
87
|
def finalize_client_loop
|
|
67
88
|
@interface = nil
|
|
68
89
|
@streams.each_key(&:stop)
|
|
90
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
69
91
|
@conn.close
|
|
70
92
|
end
|
|
71
93
|
|
|
72
94
|
def close
|
|
95
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
73
96
|
@conn.close
|
|
74
97
|
end
|
|
98
|
+
|
|
99
|
+
def set_request_for_transfer_count(request)
|
|
100
|
+
@transfer_count_request = request
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def unset_request_for_transfer_count(request)
|
|
104
|
+
return unless @transfer_count_request == request
|
|
105
|
+
|
|
106
|
+
@transfer_count_request = nil
|
|
107
|
+
end
|
|
75
108
|
end
|
|
76
109
|
end
|
data/lib/tipi/http2_stream.rb
CHANGED
|
@@ -9,13 +9,15 @@ module Tipi
|
|
|
9
9
|
attr_accessor :__next__
|
|
10
10
|
attr_reader :conn
|
|
11
11
|
|
|
12
|
-
def initialize(stream, conn, first, &block)
|
|
12
|
+
def initialize(adapter, stream, conn, first, &block)
|
|
13
|
+
@adapter = adapter
|
|
13
14
|
@stream = stream
|
|
14
15
|
@conn = conn
|
|
15
16
|
@first = first
|
|
16
17
|
@connection_fiber = Fiber.current
|
|
17
18
|
@stream_fiber = spin { |req| handle_request(req, &block) }
|
|
18
|
-
|
|
19
|
+
Thread.current.fiber_unschedule(@stream_fiber)
|
|
20
|
+
|
|
19
21
|
# Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
|
|
20
22
|
# The request handler is run on a separate fiber for each stream, allowing
|
|
21
23
|
# concurrent handling of incoming requests on the same HTTP/2 connection.
|
|
@@ -47,14 +49,17 @@ module Tipi
|
|
|
47
49
|
|
|
48
50
|
def on_headers(headers)
|
|
49
51
|
@request = Qeweney::Request.new(headers.to_h, self)
|
|
52
|
+
@request.rx_incr(@adapter.get_rx_count)
|
|
53
|
+
@request.tx_incr(@adapter.get_tx_count)
|
|
50
54
|
if @first
|
|
51
55
|
@request.headers[':first'] = true
|
|
52
56
|
@first = false
|
|
53
57
|
end
|
|
54
58
|
@stream_fiber.schedule @request
|
|
55
59
|
end
|
|
56
|
-
|
|
60
|
+
|
|
57
61
|
def on_data(data)
|
|
62
|
+
data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
|
|
58
63
|
if @waiting_for_body_chunk
|
|
59
64
|
@waiting_for_body_chunk = nil
|
|
60
65
|
@stream_fiber.schedule data
|
|
@@ -62,7 +67,7 @@ module Tipi
|
|
|
62
67
|
@request.buffer_body_chunk(data)
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
|
-
|
|
70
|
+
|
|
66
71
|
def on_half_close
|
|
67
72
|
if @waiting_for_body_chunk
|
|
68
73
|
@waiting_for_body_chunk = nil
|
|
@@ -78,62 +83,114 @@ module Tipi
|
|
|
78
83
|
def protocol
|
|
79
84
|
'h2'
|
|
80
85
|
end
|
|
86
|
+
|
|
87
|
+
def with_transfer_count(request)
|
|
88
|
+
@adapter.set_request_for_transfer_count(request)
|
|
89
|
+
yield
|
|
90
|
+
ensure
|
|
91
|
+
@adapter.unset_request_for_transfer_count(request)
|
|
92
|
+
end
|
|
81
93
|
|
|
82
|
-
def get_body_chunk
|
|
94
|
+
def get_body_chunk(request)
|
|
83
95
|
# called in the context of the stream fiber
|
|
84
96
|
return nil if @request.complete?
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
with_transfer_count(request) do
|
|
99
|
+
@waiting_for_body_chunk = true
|
|
100
|
+
# the chunk (or an exception) will be returned once the stream fiber is
|
|
101
|
+
# resumed
|
|
102
|
+
suspend
|
|
103
|
+
end
|
|
90
104
|
ensure
|
|
91
105
|
@waiting_for_body_chunk = nil
|
|
92
106
|
end
|
|
93
107
|
|
|
94
108
|
# Wait for request to finish
|
|
95
|
-
def consume_request
|
|
109
|
+
def consume_request(request)
|
|
96
110
|
return if @request.complete?
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
with_transfer_count(request) do
|
|
113
|
+
@waiting_for_half_close = true
|
|
114
|
+
suspend
|
|
115
|
+
end
|
|
100
116
|
ensure
|
|
101
117
|
@waiting_for_half_close = nil
|
|
102
118
|
end
|
|
103
119
|
|
|
104
120
|
# response API
|
|
105
|
-
def respond(chunk, headers)
|
|
121
|
+
def respond(request, chunk, headers)
|
|
106
122
|
headers[':status'] ||= Qeweney::Status::OK
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
headers[':status'] = headers[':status'].to_s
|
|
124
|
+
with_transfer_count(request) do
|
|
125
|
+
@stream.headers(transform_headers(headers))
|
|
126
|
+
@headers_sent = true
|
|
127
|
+
@stream.data(chunk || '')
|
|
128
|
+
end
|
|
129
|
+
rescue HTTP2::Error::StreamClosed
|
|
130
|
+
# ignore
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def respond_from_io(request, io, headers, chunk_size = 2**16)
|
|
134
|
+
headers[':status'] ||= Qeweney::Status::OK
|
|
135
|
+
headers[':status'] = headers[':status'].to_s
|
|
136
|
+
with_transfer_count(request) do
|
|
137
|
+
@stream.headers(transform_headers(headers))
|
|
138
|
+
@headers_sent = true
|
|
139
|
+
while (chunk = io.read(chunk_size))
|
|
140
|
+
@stream.data(chunk)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
rescue HTTP2::Error::StreamClosed
|
|
144
|
+
# ignore
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def transform_headers(headers)
|
|
148
|
+
headers.each_with_object([]) do |(k, v), a|
|
|
149
|
+
if v.is_a?(Array)
|
|
150
|
+
v.each { |vv| a << [k, vv.to_s] }
|
|
151
|
+
else
|
|
152
|
+
a << [k, v.to_s]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
110
155
|
end
|
|
111
156
|
|
|
112
|
-
def send_headers(headers, empty_response
|
|
157
|
+
def send_headers(request, headers, empty_response: false)
|
|
113
158
|
return if @headers_sent
|
|
114
159
|
|
|
115
160
|
headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
|
|
116
|
-
|
|
161
|
+
with_transfer_count(request) do
|
|
162
|
+
@stream.headers(transform_headers(headers), end_stream: false)
|
|
163
|
+
end
|
|
117
164
|
@headers_sent = true
|
|
165
|
+
rescue HTTP2::Error::StreamClosed
|
|
166
|
+
# ignore
|
|
118
167
|
end
|
|
119
168
|
|
|
120
|
-
def send_chunk(chunk, done: false)
|
|
169
|
+
def send_chunk(request, chunk, done: false)
|
|
121
170
|
send_headers({}, false) unless @headers_sent
|
|
122
171
|
|
|
123
172
|
if chunk
|
|
124
|
-
|
|
173
|
+
with_transfer_count(request) do
|
|
174
|
+
@stream.data(chunk, end_stream: done)
|
|
175
|
+
end
|
|
125
176
|
elsif done
|
|
126
177
|
@stream.close
|
|
127
178
|
end
|
|
179
|
+
rescue HTTP2::Error::StreamClosed
|
|
180
|
+
# ignore
|
|
128
181
|
end
|
|
129
182
|
|
|
130
|
-
def finish
|
|
183
|
+
def finish(request)
|
|
131
184
|
if @headers_sent
|
|
132
185
|
@stream.close
|
|
133
186
|
else
|
|
134
187
|
headers[':status'] ||= Qeweney::Status::NO_CONTENT
|
|
135
|
-
|
|
188
|
+
with_transfer_count(request) do
|
|
189
|
+
@stream.headers(transform_headers(headers), end_stream: true)
|
|
190
|
+
end
|
|
136
191
|
end
|
|
192
|
+
rescue HTTP2::Error::StreamClosed
|
|
193
|
+
# ignore
|
|
137
194
|
end
|
|
138
195
|
|
|
139
196
|
def stop
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'qeweney/request'
|
|
4
|
+
|
|
5
|
+
module Tipi
|
|
6
|
+
module ResponseExtensions
|
|
7
|
+
SPLICE_CHUNKS_SIZE_THRESHOLD = 2**20
|
|
8
|
+
|
|
9
|
+
def serve_io(io, opts)
|
|
10
|
+
if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
|
|
11
|
+
@adapter.respond_from_io(self, io, opts[:headers])
|
|
12
|
+
else
|
|
13
|
+
respond(io.read, opts[:headers] || {})
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/tipi/version.rb
CHANGED
data/test/test_http_server.rb
CHANGED
|
@@ -60,8 +60,8 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
60
60
|
connection << "GET / HTTP/1.0\r\n\r\n"
|
|
61
61
|
|
|
62
62
|
response = connection.readpartial(8192)
|
|
63
|
-
expected = <<~HTTP.chomp.http_lines
|
|
64
|
-
HTTP/1.
|
|
63
|
+
expected = <<~HTTP.chomp.http_lines.chomp
|
|
64
|
+
HTTP/1.1 200
|
|
65
65
|
Content-Length: 13
|
|
66
66
|
|
|
67
67
|
Hello, world!
|
|
@@ -78,14 +78,11 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
78
78
|
connection << "GET / HTTP/1.1\r\n\r\n"
|
|
79
79
|
|
|
80
80
|
response = connection.readpartial(8192)
|
|
81
|
-
expected = <<~HTTP.http_lines
|
|
81
|
+
expected = <<~HTTP.http_lines.chomp
|
|
82
82
|
HTTP/1.1 200
|
|
83
|
-
|
|
83
|
+
Content-Length: 13
|
|
84
84
|
|
|
85
|
-
d
|
|
86
85
|
Hello, world!
|
|
87
|
-
0
|
|
88
|
-
|
|
89
86
|
HTTP
|
|
90
87
|
assert_equal(expected, response)
|
|
91
88
|
end
|
|
@@ -98,26 +95,23 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
98
95
|
connection << "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
|
|
99
96
|
response = connection.readpartial(8192)
|
|
100
97
|
assert !connection.eof?
|
|
101
|
-
assert_equal("HTTP/1.
|
|
98
|
+
assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
|
|
102
99
|
|
|
103
100
|
connection << "GET / HTTP/1.1\r\n\r\n"
|
|
104
101
|
response = connection.readpartial(8192)
|
|
105
102
|
assert !connection.eof?
|
|
106
|
-
expected = <<~HTTP.http_lines
|
|
103
|
+
expected = <<~HTTP.http_lines.chomp
|
|
107
104
|
HTTP/1.1 200
|
|
108
|
-
|
|
105
|
+
Content-Length: 2
|
|
109
106
|
|
|
110
|
-
2
|
|
111
107
|
Hi
|
|
112
|
-
0
|
|
113
|
-
|
|
114
108
|
HTTP
|
|
115
109
|
assert_equal(expected, response)
|
|
116
110
|
|
|
117
111
|
connection << "GET / HTTP/1.0\r\n\r\n"
|
|
118
112
|
response = connection.readpartial(8192)
|
|
119
113
|
assert connection.eof?
|
|
120
|
-
assert_equal("HTTP/1.
|
|
114
|
+
assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
|
|
121
115
|
end
|
|
122
116
|
|
|
123
117
|
def test_pipelining_client
|
|
@@ -130,24 +124,17 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
130
124
|
end
|
|
131
125
|
|
|
132
126
|
connection << "GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nFoo: bar\r\n\r\n"
|
|
133
|
-
|
|
127
|
+
sleep 0.01
|
|
134
128
|
response = connection.readpartial(8192)
|
|
135
129
|
|
|
136
|
-
expected = <<~HTTP.http_lines
|
|
130
|
+
expected = <<~HTTP.http_lines.chomp
|
|
137
131
|
HTTP/1.1 200
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
d
|
|
141
|
-
Hello, world!
|
|
142
|
-
0
|
|
132
|
+
Content-Length: 13
|
|
143
133
|
|
|
144
|
-
HTTP/1.1 200
|
|
145
|
-
|
|
134
|
+
Hello, world!HTTP/1.1 200
|
|
135
|
+
Content-Length: 14
|
|
146
136
|
|
|
147
|
-
e
|
|
148
137
|
Hello, foobar!
|
|
149
|
-
0
|
|
150
|
-
|
|
151
138
|
HTTP
|
|
152
139
|
assert_equal(expected, response)
|
|
153
140
|
end
|
|
@@ -172,22 +159,22 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
172
159
|
6
|
|
173
160
|
foobar
|
|
174
161
|
HTTP
|
|
175
|
-
|
|
162
|
+
sleep 0.01
|
|
176
163
|
assert request
|
|
177
164
|
assert_equal %w[foobar], chunks
|
|
178
165
|
assert !request.complete?
|
|
179
166
|
|
|
180
167
|
connection << "6\r\nbazbud\r\n"
|
|
181
|
-
|
|
168
|
+
sleep 0.01
|
|
182
169
|
assert_equal %w[foobar bazbud], chunks
|
|
183
170
|
assert !request.complete?
|
|
184
171
|
|
|
185
172
|
connection << "0\r\n\r\n"
|
|
186
|
-
|
|
173
|
+
sleep 0.01
|
|
187
174
|
assert_equal %w[foobar bazbud], chunks
|
|
188
175
|
assert request.complete?
|
|
189
176
|
|
|
190
|
-
|
|
177
|
+
sleep 0.01
|
|
191
178
|
|
|
192
179
|
response = connection.readpartial(8192)
|
|
193
180
|
|
|
@@ -210,7 +197,8 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
210
197
|
|
|
211
198
|
opts = {
|
|
212
199
|
upgrade: {
|
|
213
|
-
echo: lambda do |
|
|
200
|
+
echo: lambda do |adapter, _headers|
|
|
201
|
+
conn = adapter.conn
|
|
214
202
|
conn << <<~HTTP.http_lines
|
|
215
203
|
HTTP/1.1 101 Switching Protocols
|
|
216
204
|
Upgrade: echo
|
|
@@ -231,14 +219,11 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
231
219
|
connection << "GET / HTTP/1.1\r\n\r\n"
|
|
232
220
|
response = connection.readpartial(8192)
|
|
233
221
|
assert !connection.eof?
|
|
234
|
-
expected = <<~HTTP.http_lines
|
|
222
|
+
expected = <<~HTTP.http_lines.chomp
|
|
235
223
|
HTTP/1.1 200
|
|
236
|
-
|
|
224
|
+
Content-Length: 2
|
|
237
225
|
|
|
238
|
-
2
|
|
239
226
|
Hi
|
|
240
|
-
0
|
|
241
|
-
|
|
242
227
|
HTTP
|
|
243
228
|
assert_equal(expected, response)
|
|
244
229
|
|
|
@@ -271,7 +256,7 @@ class HTTP1ServerTest < MiniTest::Test
|
|
|
271
256
|
connection.close
|
|
272
257
|
assert !done
|
|
273
258
|
|
|
274
|
-
|
|
259
|
+
sleep 0.01
|
|
275
260
|
assert done
|
|
276
261
|
end
|
|
277
262
|
|
data/test/test_request.rb
CHANGED
|
@@ -60,7 +60,7 @@ class RequestHeadersTest < MiniTest::Test
|
|
|
60
60
|
|
|
61
61
|
connection << "GET /titi HTTP/1.1\r\nHost: blah.com\r\nFoo: bar\r\nhi: 1\r\nHi: 2\r\nhi: 3\r\n\r\n"
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
sleep 0.01
|
|
64
64
|
|
|
65
65
|
assert_kind_of Qeweney::Request, req
|
|
66
66
|
assert_equal 'blah.com', req.headers['host']
|
|
@@ -78,7 +78,7 @@ class RequestHeadersTest < MiniTest::Test
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
connection << "GET /titi HTTP/1.1\nHost: blah.com\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
|
|
81
|
-
|
|
81
|
+
sleep 0.01
|
|
82
82
|
assert_equal 'blah.com', req.host
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -90,7 +90,7 @@ class RequestHeadersTest < MiniTest::Test
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
connection << "GET /titi HTTP/1.1\nConnection: keep-alive\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
|
|
93
|
-
|
|
93
|
+
sleep 0.01
|
|
94
94
|
assert_equal 'keep-alive', req.connection
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -102,7 +102,7 @@ class RequestHeadersTest < MiniTest::Test
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
connection << "GET /titi HTTP/1.1\nConnection: upgrade\nUpgrade: foobar\n\n"
|
|
105
|
-
|
|
105
|
+
sleep 0.01
|
|
106
106
|
assert_equal 'foobar', req.upgrade_protocol
|
|
107
107
|
end
|
|
108
108
|
end
|