httpx 0.0.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 +7 -0
- data/LICENSE.txt +191 -0
- data/README.md +119 -0
- data/lib/httpx.rb +50 -0
- data/lib/httpx/buffer.rb +34 -0
- data/lib/httpx/callbacks.rb +32 -0
- data/lib/httpx/chainable.rb +51 -0
- data/lib/httpx/channel.rb +222 -0
- data/lib/httpx/channel/http1.rb +220 -0
- data/lib/httpx/channel/http2.rb +224 -0
- data/lib/httpx/client.rb +173 -0
- data/lib/httpx/connection.rb +74 -0
- data/lib/httpx/errors.rb +7 -0
- data/lib/httpx/extensions.rb +52 -0
- data/lib/httpx/headers.rb +152 -0
- data/lib/httpx/io.rb +240 -0
- data/lib/httpx/loggable.rb +11 -0
- data/lib/httpx/options.rb +138 -0
- data/lib/httpx/plugins/authentication.rb +14 -0
- data/lib/httpx/plugins/basic_authentication.rb +20 -0
- data/lib/httpx/plugins/compression.rb +123 -0
- data/lib/httpx/plugins/compression/brotli.rb +55 -0
- data/lib/httpx/plugins/compression/deflate.rb +50 -0
- data/lib/httpx/plugins/compression/gzip.rb +59 -0
- data/lib/httpx/plugins/cookies.rb +63 -0
- data/lib/httpx/plugins/digest_authentication.rb +141 -0
- data/lib/httpx/plugins/follow_redirects.rb +72 -0
- data/lib/httpx/plugins/h2c.rb +85 -0
- data/lib/httpx/plugins/proxy.rb +108 -0
- data/lib/httpx/plugins/proxy/http.rb +115 -0
- data/lib/httpx/plugins/proxy/socks4.rb +110 -0
- data/lib/httpx/plugins/proxy/socks5.rb +152 -0
- data/lib/httpx/plugins/push_promise.rb +67 -0
- data/lib/httpx/plugins/stream.rb +33 -0
- data/lib/httpx/registry.rb +88 -0
- data/lib/httpx/request.rb +222 -0
- data/lib/httpx/response.rb +225 -0
- data/lib/httpx/selector.rb +155 -0
- data/lib/httpx/timeout.rb +68 -0
- data/lib/httpx/transcoder.rb +12 -0
- data/lib/httpx/transcoder/body.rb +56 -0
- data/lib/httpx/transcoder/chunker.rb +38 -0
- data/lib/httpx/transcoder/form.rb +41 -0
- data/lib/httpx/transcoder/json.rb +36 -0
- data/lib/httpx/version.rb +5 -0
- metadata +150 -0
@@ -0,0 +1,222 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "httpx/io"
|
5
|
+
require "httpx/buffer"
|
6
|
+
|
7
|
+
module HTTPX
|
8
|
+
# The Channel entity can be watched for IO events.
|
9
|
+
#
|
10
|
+
# It contains the +io+ object to read/write from, and knows what to do when it can.
|
11
|
+
#
|
12
|
+
# It defers connecting until absolutely necessary. Connection should be triggered from
|
13
|
+
# the IO selector (until then, any request will be queued).
|
14
|
+
#
|
15
|
+
# A channel boots up its parser after connection is established. All pending requests
|
16
|
+
# will be redirected there after connection.
|
17
|
+
#
|
18
|
+
# A channel can be prevented from closing by the parser, that is, if there are pending
|
19
|
+
# requests. This will signal that the channel was prematurely closed, due to a possible
|
20
|
+
# number of conditions:
|
21
|
+
#
|
22
|
+
# * Remote peer closed the connection ("Connection: close");
|
23
|
+
# * Remote peer doesn't support pipelining;
|
24
|
+
#
|
25
|
+
# A channel may also route requests for a different host for which the +io+ was connected
|
26
|
+
# to, provided that the IP is the same and the port and scheme as well. This will allow to
|
27
|
+
# share the same socket to send HTTP/2 requests to different hosts.
|
28
|
+
# TODO: For this to succeed, the certificates sent by the servers to the client must be
|
29
|
+
# identical (or match both hosts).
|
30
|
+
#
|
31
|
+
class Channel
|
32
|
+
extend Forwardable
|
33
|
+
include Registry
|
34
|
+
include Loggable
|
35
|
+
include Callbacks
|
36
|
+
|
37
|
+
require "httpx/channel/http2"
|
38
|
+
require "httpx/channel/http1"
|
39
|
+
|
40
|
+
BUFFER_SIZE = 1 << 14
|
41
|
+
|
42
|
+
class << self
|
43
|
+
def by(uri, options)
|
44
|
+
io = case uri.scheme
|
45
|
+
when "http"
|
46
|
+
IO.registry("tcp").new(uri.host, uri.port, options)
|
47
|
+
when "https"
|
48
|
+
IO.registry("ssl").new(uri.host, uri.port, options)
|
49
|
+
else
|
50
|
+
raise Error, "#{uri}: #{uri.scheme}: unrecognized channel"
|
51
|
+
end
|
52
|
+
new(io, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def_delegator :@io, :closed?
|
57
|
+
|
58
|
+
def_delegator :@write_buffer, :empty?
|
59
|
+
|
60
|
+
def initialize(io, options)
|
61
|
+
@io = io
|
62
|
+
@options = Options.new(options)
|
63
|
+
@window_size = @options.window_size
|
64
|
+
@read_buffer = Buffer.new(BUFFER_SIZE)
|
65
|
+
@write_buffer = Buffer.new(BUFFER_SIZE)
|
66
|
+
@pending = []
|
67
|
+
@state = :idle
|
68
|
+
end
|
69
|
+
|
70
|
+
def match?(uri)
|
71
|
+
ip = begin
|
72
|
+
TCPSocket.getaddress(uri.host)
|
73
|
+
rescue StandardError
|
74
|
+
uri.host
|
75
|
+
end
|
76
|
+
|
77
|
+
ip == @io.ip &&
|
78
|
+
uri.port == @io.port &&
|
79
|
+
uri.scheme == @io.scheme
|
80
|
+
end
|
81
|
+
|
82
|
+
def interests
|
83
|
+
return :w if @state == :idle
|
84
|
+
readable = !@read_buffer.full?
|
85
|
+
writable = !@write_buffer.empty?
|
86
|
+
if readable
|
87
|
+
writable ? :rw : :r
|
88
|
+
else
|
89
|
+
writable ? :w : :r
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_io
|
94
|
+
case @state
|
95
|
+
when :idle
|
96
|
+
transition(:open)
|
97
|
+
end
|
98
|
+
@io.to_io
|
99
|
+
end
|
100
|
+
|
101
|
+
def close(hard = false)
|
102
|
+
pr = @parser
|
103
|
+
transition(:closing)
|
104
|
+
if hard || (pr && pr.empty?)
|
105
|
+
pr.close
|
106
|
+
@parser = nil
|
107
|
+
else
|
108
|
+
transition(:idle)
|
109
|
+
@parser = pr
|
110
|
+
parser.reenqueue!
|
111
|
+
return
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def reset
|
116
|
+
transition(:closing)
|
117
|
+
transition(:closed)
|
118
|
+
emit(:close)
|
119
|
+
end
|
120
|
+
|
121
|
+
def send(request, **args)
|
122
|
+
if @parser && !@write_buffer.full?
|
123
|
+
parser.send(request, **args)
|
124
|
+
else
|
125
|
+
@pending << [request, args]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def call
|
130
|
+
case @state
|
131
|
+
when :closed
|
132
|
+
return
|
133
|
+
when :closing
|
134
|
+
dwrite
|
135
|
+
transition(:closed)
|
136
|
+
emit(:close)
|
137
|
+
else
|
138
|
+
catch(:called) do
|
139
|
+
dread
|
140
|
+
dwrite
|
141
|
+
parser.consume
|
142
|
+
end
|
143
|
+
end
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
def upgrade_parser(protocol)
|
148
|
+
@parser.reset if @parser
|
149
|
+
@parser = build_parser(protocol)
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def dread(wsize = @window_size)
|
155
|
+
loop do
|
156
|
+
siz = @io.read(wsize, @read_buffer)
|
157
|
+
throw(:close, self) unless siz
|
158
|
+
return if siz.zero?
|
159
|
+
log { "READ: #{siz} bytes..." }
|
160
|
+
parser << @read_buffer.to_s
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def dwrite
|
165
|
+
loop do
|
166
|
+
return if @write_buffer.empty?
|
167
|
+
siz = @io.write(@write_buffer)
|
168
|
+
throw(:close, self) unless siz
|
169
|
+
log { "WRITE: #{siz} bytes..." }
|
170
|
+
return if siz.zero?
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def send_pending
|
175
|
+
while !@write_buffer.full? && (req_args = @pending.shift)
|
176
|
+
request, args = req_args
|
177
|
+
parser.send(request, **args)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def parser
|
182
|
+
@parser ||= build_parser
|
183
|
+
end
|
184
|
+
|
185
|
+
def build_parser(protocol = @io.protocol)
|
186
|
+
parser = registry(protocol).new(@write_buffer, @options)
|
187
|
+
parser.on(:response) do |*args|
|
188
|
+
emit(:response, *args)
|
189
|
+
end
|
190
|
+
parser.on(:promise) do |*args|
|
191
|
+
emit(:promise, *args)
|
192
|
+
end
|
193
|
+
# parser.inherit_callbacks(self)
|
194
|
+
parser.on(:complete) { throw(:close, self) }
|
195
|
+
parser.on(:close) do
|
196
|
+
transition(:closed)
|
197
|
+
emit(:close)
|
198
|
+
end
|
199
|
+
parser
|
200
|
+
end
|
201
|
+
|
202
|
+
def transition(nextstate)
|
203
|
+
case nextstate
|
204
|
+
# when :idle
|
205
|
+
|
206
|
+
when :open
|
207
|
+
return if @state == :closed
|
208
|
+
@io.connect
|
209
|
+
return if @io.closed?
|
210
|
+
send_pending
|
211
|
+
when :closing
|
212
|
+
return unless @state == :open
|
213
|
+
when :closed
|
214
|
+
return unless @state == :closing
|
215
|
+
return unless @write_buffer.empty?
|
216
|
+
@io.close
|
217
|
+
@read_buffer.clear
|
218
|
+
end
|
219
|
+
@state = nextstate
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "http_parser"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
class Channel::HTTP1
|
7
|
+
include Callbacks
|
8
|
+
include Loggable
|
9
|
+
|
10
|
+
CRLF = "\r\n"
|
11
|
+
|
12
|
+
def initialize(buffer, options)
|
13
|
+
@options = Options.new(options)
|
14
|
+
@max_concurrent_requests = @options.max_concurrent_requests
|
15
|
+
@retries = options.max_retries
|
16
|
+
@parser = HTTP::Parser.new(self)
|
17
|
+
@parser.header_value_type = :arrays
|
18
|
+
@buffer = buffer
|
19
|
+
@version = [1, 1]
|
20
|
+
@pending = []
|
21
|
+
@requests = []
|
22
|
+
@has_response = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset
|
26
|
+
@parser.reset!
|
27
|
+
@has_response = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def close
|
31
|
+
reset
|
32
|
+
emit(:close)
|
33
|
+
end
|
34
|
+
|
35
|
+
def empty?
|
36
|
+
# this means that for every request there's an available
|
37
|
+
# partial response, so there are no in-flight requests waiting.
|
38
|
+
@requests.empty? || @requests.all? { |request| !request.response.nil? }
|
39
|
+
end
|
40
|
+
|
41
|
+
def <<(data)
|
42
|
+
@parser << data
|
43
|
+
dispatch if @has_response
|
44
|
+
end
|
45
|
+
|
46
|
+
def send(request, **)
|
47
|
+
if @requests.size >= @max_concurrent_requests
|
48
|
+
@pending << request
|
49
|
+
return
|
50
|
+
end
|
51
|
+
@requests << request unless @requests.include?(request)
|
52
|
+
handle(request)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reenqueue!
|
56
|
+
requests = @requests.dup
|
57
|
+
@requests.clear
|
58
|
+
requests.each do |request|
|
59
|
+
send(request)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def consume
|
64
|
+
@requests.each do |request|
|
65
|
+
handle(request)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# HTTP Parser callbacks
|
70
|
+
#
|
71
|
+
# must be public methods, or else they won't be reachable
|
72
|
+
|
73
|
+
def on_message_begin
|
74
|
+
log(2) { "parsing begins" }
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_headers_complete(h)
|
78
|
+
return on_trailer_headers_complete(h) if @parser_trailers
|
79
|
+
# Wait for fix: https://github.com/tmm1/http_parser.rb/issues/52
|
80
|
+
# callback is called 2 times when chunked
|
81
|
+
request = @requests.first
|
82
|
+
return if request.response
|
83
|
+
|
84
|
+
log(2) { "headers received" }
|
85
|
+
headers = @options.headers_class.new(h)
|
86
|
+
response = @options.response_class.new(@requests.last,
|
87
|
+
@parser.status_code,
|
88
|
+
@parser.http_version.join("."),
|
89
|
+
headers, @options)
|
90
|
+
log { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
|
91
|
+
log { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
|
92
|
+
|
93
|
+
request.response = response
|
94
|
+
|
95
|
+
@has_response = true if response.complete?
|
96
|
+
end
|
97
|
+
|
98
|
+
def on_body(chunk)
|
99
|
+
log { "-> DATA: #{chunk.bytesize} bytes..." }
|
100
|
+
log(2) { "-> #{chunk.inspect}" }
|
101
|
+
response = @requests.first.response
|
102
|
+
|
103
|
+
response << chunk
|
104
|
+
|
105
|
+
dispatch if response.complete?
|
106
|
+
end
|
107
|
+
|
108
|
+
def on_message_complete
|
109
|
+
log(2) { "parsing complete" }
|
110
|
+
request = @requests.first
|
111
|
+
response = request.response
|
112
|
+
|
113
|
+
if !@parser_trailers && response.headers.key?("trailer")
|
114
|
+
@parser_trailers = true
|
115
|
+
# this is needed, because the parser can't accept further headers.
|
116
|
+
# we need to reset it and artificially move it to receive headers state,
|
117
|
+
# hence the bogus headline
|
118
|
+
#
|
119
|
+
@parser.reset!
|
120
|
+
@parser << "#{request.verb.to_s.upcase} #{request.path} HTTP/#{response.version}#{CRLF}"
|
121
|
+
else
|
122
|
+
@has_response = true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_trailer_headers_complete(h)
|
127
|
+
response = @requests.first.response
|
128
|
+
|
129
|
+
response.merge_headers(h)
|
130
|
+
end
|
131
|
+
|
132
|
+
def dispatch
|
133
|
+
request = @requests.first
|
134
|
+
return handle(request) if request.expects?
|
135
|
+
|
136
|
+
@requests.shift
|
137
|
+
response = request.response
|
138
|
+
emit(:response, request, response)
|
139
|
+
|
140
|
+
if @parser.upgrade?
|
141
|
+
response << @parser.upgrade_data
|
142
|
+
throw(:called)
|
143
|
+
end
|
144
|
+
close
|
145
|
+
send(@pending.shift) unless @pending.empty?
|
146
|
+
return unless response.headers["connection"] == "close"
|
147
|
+
unless @requests.empty?
|
148
|
+
@requests.map { |r| r.transition(:idle) }
|
149
|
+
# server doesn't handle pipelining, and probably
|
150
|
+
# doesn't support keep-alive. Fallback to send only
|
151
|
+
# 1 keep alive request.
|
152
|
+
@max_concurrent_requests = 1
|
153
|
+
end
|
154
|
+
emit(:complete)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def set_request_headers(request)
|
160
|
+
request.headers["host"] ||= request.authority
|
161
|
+
request.headers["connection"] ||= "keep-alive"
|
162
|
+
if !request.headers.key?("content-length") &&
|
163
|
+
request.body.bytesize == Float::INFINITY
|
164
|
+
request.chunk!
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def headline_uri(request)
|
169
|
+
request.path
|
170
|
+
end
|
171
|
+
|
172
|
+
def handle(request)
|
173
|
+
@has_response = false
|
174
|
+
set_request_headers(request)
|
175
|
+
catch(:buffer_full) do
|
176
|
+
request.transition(:headers)
|
177
|
+
join_headers(request) if request.state == :headers
|
178
|
+
request.transition(:body)
|
179
|
+
join_body(request) if request.state == :body
|
180
|
+
request.transition(:done)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def join_headers(request)
|
185
|
+
buffer = +""
|
186
|
+
buffer << "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}" << CRLF
|
187
|
+
log { "<- HEADLINE: #{buffer.chomp.inspect}" }
|
188
|
+
@buffer << buffer
|
189
|
+
buffer.clear
|
190
|
+
request.headers.each do |field, value|
|
191
|
+
buffer << "#{capitalized(field)}: #{value}" << CRLF
|
192
|
+
log { "<- HEADER: #{buffer.chomp}" }
|
193
|
+
@buffer << buffer
|
194
|
+
buffer.clear
|
195
|
+
end
|
196
|
+
log { "<- " }
|
197
|
+
@buffer << CRLF
|
198
|
+
end
|
199
|
+
|
200
|
+
def join_body(request)
|
201
|
+
return if request.empty?
|
202
|
+
while (chunk = request.drain_body)
|
203
|
+
log { "<- DATA: #{chunk.bytesize} bytes..." }
|
204
|
+
log(2) { "<- #{chunk.inspect}" }
|
205
|
+
@buffer << chunk
|
206
|
+
throw(:buffer_full, request) if @buffer.full?
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
UPCASED = {
|
211
|
+
"www-authenticate" => "WWW-Authenticate",
|
212
|
+
"http2-settings" => "HTTP2-Settings",
|
213
|
+
}.freeze
|
214
|
+
|
215
|
+
def capitalized(field)
|
216
|
+
UPCASED[field] || field.to_s.split("-").map(&:capitalize).join("-")
|
217
|
+
end
|
218
|
+
end
|
219
|
+
Channel.register "http/1.1", Channel::HTTP1
|
220
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "http/2"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
class Channel::HTTP2
|
7
|
+
include Callbacks
|
8
|
+
include Loggable
|
9
|
+
|
10
|
+
attr_reader :streams, :pending
|
11
|
+
|
12
|
+
def initialize(buffer, options)
|
13
|
+
@options = Options.new(options)
|
14
|
+
@max_concurrent_requests = @options.max_concurrent_requests
|
15
|
+
init_connection
|
16
|
+
@retries = options.max_retries
|
17
|
+
@pending = []
|
18
|
+
@streams = {}
|
19
|
+
@drains = {}
|
20
|
+
@buffer = buffer
|
21
|
+
end
|
22
|
+
|
23
|
+
def close
|
24
|
+
@connection.goaway
|
25
|
+
end
|
26
|
+
|
27
|
+
def empty?
|
28
|
+
@connection.state == :closed || @streams.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def <<(data)
|
32
|
+
@connection << data
|
33
|
+
end
|
34
|
+
|
35
|
+
def send(request, **)
|
36
|
+
if @connection.active_stream_count >= @max_concurrent_requests
|
37
|
+
@pending << request
|
38
|
+
return
|
39
|
+
end
|
40
|
+
unless (stream = @streams[request])
|
41
|
+
stream = @connection.new_stream
|
42
|
+
handle_stream(stream, request)
|
43
|
+
@streams[request] = stream
|
44
|
+
end
|
45
|
+
handle(request, stream)
|
46
|
+
end
|
47
|
+
|
48
|
+
def reenqueue!
|
49
|
+
requests = @streams.keys
|
50
|
+
@streams.clear
|
51
|
+
init_connection
|
52
|
+
requests.each do |request|
|
53
|
+
send(request)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def consume
|
58
|
+
@streams.each do |request, stream|
|
59
|
+
handle(request, stream)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def headline_uri(request)
|
66
|
+
request.path
|
67
|
+
end
|
68
|
+
|
69
|
+
def set_request_headers(request); end
|
70
|
+
|
71
|
+
def handle(request, stream)
|
72
|
+
catch(:buffer_full) do
|
73
|
+
request.transition(:headers)
|
74
|
+
join_headers(stream, request) if request.state == :headers
|
75
|
+
request.transition(:body)
|
76
|
+
join_body(stream, request) if request.state == :body
|
77
|
+
request.transition(:done)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def init_connection
|
82
|
+
@connection = HTTP2::Client.new(@options.http2_settings)
|
83
|
+
@connection.on(:frame, &method(:on_frame))
|
84
|
+
@connection.on(:frame_sent, &method(:on_frame_sent))
|
85
|
+
@connection.on(:frame_received, &method(:on_frame_received))
|
86
|
+
@connection.on(:promise, &method(:on_promise))
|
87
|
+
@connection.on(:altsvc, &method(:on_altsvc))
|
88
|
+
@connection.on(:settings_ack, &method(:on_settings))
|
89
|
+
@connection.on(:goaway, &method(:on_close))
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_stream(stream, request)
|
93
|
+
stream.on(:close, &method(:on_stream_close).curry[stream, request])
|
94
|
+
stream.on(:half_close) do
|
95
|
+
log(2, "#{stream.id}: ") { "waiting for response..." }
|
96
|
+
end
|
97
|
+
# stream.on(:altsvc)
|
98
|
+
stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
|
99
|
+
stream.on(:data, &method(:on_stream_data).curry[stream, request])
|
100
|
+
end
|
101
|
+
|
102
|
+
def join_headers(stream, request)
|
103
|
+
set_request_headers(request)
|
104
|
+
headers = {}
|
105
|
+
headers[":scheme"] = request.scheme
|
106
|
+
headers[":method"] = request.verb.to_s.upcase
|
107
|
+
headers[":path"] = headline_uri(request)
|
108
|
+
headers[":authority"] = request.authority
|
109
|
+
headers = headers.merge(request.headers)
|
110
|
+
log(1, "#{stream.id}: ") do
|
111
|
+
headers.map { |k, v| "-> HEADER: #{k}: #{v}" }.join("\n")
|
112
|
+
end
|
113
|
+
stream.headers(headers, end_stream: request.empty?)
|
114
|
+
end
|
115
|
+
|
116
|
+
def join_body(stream, request)
|
117
|
+
chunk = @drains.delete(request) || request.drain_body
|
118
|
+
while chunk
|
119
|
+
next_chunk = request.drain_body
|
120
|
+
log(1, "#{stream.id}: ") { "-> DATA: #{chunk.bytesize} bytes..." }
|
121
|
+
log(2, "#{stream.id}: ") { "-> #{chunk.inspect}" }
|
122
|
+
stream.data(chunk, end_stream: !next_chunk)
|
123
|
+
if next_chunk && @buffer.full?
|
124
|
+
@drains[request] = next_chunk
|
125
|
+
throw(:buffer_full)
|
126
|
+
end
|
127
|
+
chunk = next_chunk
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
######
|
132
|
+
# HTTP/2 Callbacks
|
133
|
+
######
|
134
|
+
|
135
|
+
def on_stream_headers(stream, request, h)
|
136
|
+
log(stream.id) do
|
137
|
+
h.map { |k, v| "<- HEADER: #{k}: #{v}" }.join("\n")
|
138
|
+
end
|
139
|
+
_, status = h.shift
|
140
|
+
headers = @options.headers_class.new(h)
|
141
|
+
response = @options.response_class.new(request, status, "2.0", headers, @options)
|
142
|
+
request.response = response
|
143
|
+
@streams[request] = stream
|
144
|
+
end
|
145
|
+
|
146
|
+
def on_stream_data(stream, request, data)
|
147
|
+
log(1, "#{stream.id}: ") { "<- DATA: #{data.bytesize} bytes..." }
|
148
|
+
log(2, "#{stream.id}: ") { "<- #{data.inspect}" }
|
149
|
+
request.response << data
|
150
|
+
end
|
151
|
+
|
152
|
+
def on_stream_close(stream, request, error)
|
153
|
+
return handle(request, stream) if request.expects?
|
154
|
+
response = request.response || ErrorResponse.new(error, @retries)
|
155
|
+
emit(:response, request, response)
|
156
|
+
log(2, "#{stream.id}: ") { "closing stream" }
|
157
|
+
|
158
|
+
@streams.delete(request)
|
159
|
+
send(@pending.shift) unless @pending.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
def on_frame(bytes)
|
163
|
+
@buffer << bytes
|
164
|
+
end
|
165
|
+
|
166
|
+
def on_settings(*)
|
167
|
+
@max_concurrent_requests = [@max_concurrent_requests,
|
168
|
+
@connection.remote_settings[:settings_max_concurrent_streams]].min
|
169
|
+
end
|
170
|
+
|
171
|
+
def on_close(*)
|
172
|
+
return unless @connection.state == :closed && @connection.active_stream_count.zero?
|
173
|
+
emit(:complete)
|
174
|
+
end
|
175
|
+
|
176
|
+
def on_frame_sent(frame)
|
177
|
+
log(2, "#{frame[:stream]}: ") { "frame was sent!" }
|
178
|
+
log(2, "#{frame[:stream]}: ") do
|
179
|
+
case frame[:type]
|
180
|
+
when :data
|
181
|
+
frame.merge(payload: frame[:payload].bytesize).inspect
|
182
|
+
when :headers
|
183
|
+
"\e[33m#{frame.inspect}\e[0m"
|
184
|
+
else
|
185
|
+
frame.inspect
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def on_frame_received(frame)
|
191
|
+
log(2, "#{frame[:stream]}: ") { "frame was received!" }
|
192
|
+
log(2, "#{frame[:stream]}: ") do
|
193
|
+
case frame[:type]
|
194
|
+
when :data
|
195
|
+
frame.merge(payload: frame[:payload].bytesize).inspect
|
196
|
+
else
|
197
|
+
frame.inspect
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def on_altsvc(frame)
|
203
|
+
log(2, "#{frame[:stream]}: ") { "altsvc frame was received" }
|
204
|
+
log(2, "#{frame[:stream]}: ") { frame.inspect }
|
205
|
+
end
|
206
|
+
|
207
|
+
def on_promise(stream)
|
208
|
+
emit(:promise, self, stream)
|
209
|
+
end
|
210
|
+
|
211
|
+
def respond_to_missing?(meth, *args)
|
212
|
+
@connection.respond_to?(meth, *args) || super
|
213
|
+
end
|
214
|
+
|
215
|
+
def method_missing(meth, *args, &blk)
|
216
|
+
if @connection.respond_to?(meth)
|
217
|
+
@connection.__send__(meth, *args, &blk)
|
218
|
+
else
|
219
|
+
super
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
Channel.register "h2", Channel::HTTP2
|
224
|
+
end
|