httpx 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "io/wait"
3
4
  require "http/2"
4
5
 
5
6
  module HTTPX
@@ -7,6 +8,22 @@ module HTTPX
7
8
  include Callbacks
8
9
  include Loggable
9
10
 
11
+ if HTTP2::VERSION < "0.10.1"
12
+ module HTTP2Extensions
13
+ refine ::HTTP2::Client do
14
+ def receive(*)
15
+ send_connection_preface
16
+ super
17
+ end
18
+
19
+ def <<(*args)
20
+ receive(*args)
21
+ end
22
+ end
23
+ end
24
+ using HTTP2Extensions
25
+ end
26
+
10
27
  Error = Class.new(Error) do
11
28
  def initialize(id, code)
12
29
  super("stream #{id} closed with error: #{code}")
@@ -18,11 +35,12 @@ module HTTPX
18
35
  def initialize(buffer, options)
19
36
  @options = Options.new(options)
20
37
  @max_concurrent_requests = @options.max_concurrent_requests
21
- init_connection
22
38
  @pending = []
23
39
  @streams = {}
24
40
  @drains = {}
25
41
  @buffer = buffer
42
+ @handshake_completed = false
43
+ init_connection
26
44
  end
27
45
 
28
46
  def close
@@ -38,7 +56,8 @@ module HTTPX
38
56
  end
39
57
 
40
58
  def send(request, **)
41
- if @connection.active_stream_count >= @max_concurrent_requests
59
+ if !@handshake_completed ||
60
+ @connection.active_stream_count >= @max_concurrent_requests
42
61
  @pending << request
43
62
  return
44
63
  end
@@ -48,6 +67,7 @@ module HTTPX
48
67
  @streams[request] = stream
49
68
  end
50
69
  handle(request, stream)
70
+ true
51
71
  end
52
72
 
53
73
  def reenqueue!
@@ -73,6 +93,12 @@ module HTTPX
73
93
 
74
94
  private
75
95
 
96
+ def send_pending
97
+ while (request = @pending.shift)
98
+ break unless send(request)
99
+ end
100
+ end
101
+
76
102
  def headline_uri(request)
77
103
  request.path
78
104
  end
@@ -95,9 +121,16 @@ module HTTPX
95
121
  @connection.on(:frame_sent, &method(:on_frame_sent))
96
122
  @connection.on(:frame_received, &method(:on_frame_received))
97
123
  @connection.on(:promise, &method(:on_promise))
98
- @connection.on(:altsvc, &method(:on_altsvc))
124
+ @connection.on(:altsvc) { |frame| on_altsvc(frame[:origin], frame) }
99
125
  @connection.on(:settings_ack, &method(:on_settings))
100
126
  @connection.on(:goaway, &method(:on_close))
127
+ #
128
+ # Some servers initiate HTTP/2 negotiation right away, some don't.
129
+ # As such, we have to check the socket buffer. If there is something
130
+ # to read, the server initiated the negotiation. If not, we have to
131
+ # initiate it.
132
+ #
133
+ @connection.send_connection_preface
101
134
  end
102
135
 
103
136
  def handle_stream(stream, request)
@@ -105,7 +138,7 @@ module HTTPX
105
138
  stream.on(:half_close) do
106
139
  log(level: 2, label: "#{stream.id}: ") { "waiting for response..." }
107
140
  end
108
- # stream.on(:altsvc)
141
+ stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
109
142
  stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
110
143
  stream.on(:data, &method(:on_stream_data).curry[stream, request])
111
144
  end
@@ -187,8 +220,10 @@ module HTTPX
187
220
  end
188
221
 
189
222
  def on_settings(*)
223
+ @handshake_completed = true
190
224
  @max_concurrent_requests = [@max_concurrent_requests,
191
225
  @connection.remote_settings[:settings_max_concurrent_streams]].min
226
+ send_pending
192
227
  end
193
228
 
194
229
  def on_close(_last_frame, error, _payload)
@@ -225,9 +260,12 @@ module HTTPX
225
260
  end
226
261
  end
227
262
 
228
- def on_altsvc(frame)
263
+ def on_altsvc(origin, frame)
229
264
  log(level: 2, label: "#{frame[:stream]}: ") { "altsvc frame was received" }
230
265
  log(level: 2, label: "#{frame[:stream]}: ") { frame.inspect }
266
+ alt_origin = URI.parse("#{frame[:proto]}://#{frame[:host]}:#{frame[:port]}")
267
+ params = { "ma" => frame[:max_age] }
268
+ emit(:altsvc, origin, alt_origin, origin, params)
231
269
  end
232
270
 
233
271
  def on_promise(stream)
@@ -47,7 +47,6 @@ module HTTPX
47
47
  def on_promise(_, stream)
48
48
  log(level: 2, label: "#{stream.id}: ") { "refusing stream!" }
49
49
  stream.refuse
50
- # TODO: policy for handling promises
51
50
  end
52
51
 
53
52
  def fetch_response(request)
@@ -66,6 +65,9 @@ module HTTPX
66
65
  other_channel = build_channel(uncoalesced_uri, options)
67
66
  channel.unmerge(other_channel)
68
67
  end
68
+ channel.on(:altsvc) do |alt_origin, origin, alt_params|
69
+ build_altsvc_channel(channel, alt_origin, origin, alt_params, options)
70
+ end
69
71
  end
70
72
 
71
73
  def build_channel(uri, options)
@@ -74,6 +76,35 @@ module HTTPX
74
76
  channel
75
77
  end
76
78
 
79
+ def build_altsvc_channel(existing_channel, alt_origin, origin, alt_params, options)
80
+ altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
81
+
82
+ # altsvc already exists, somehow it wasn't advertised, probably noop
83
+ return unless altsvc
84
+
85
+ channel = @connection.find_channel(alt_origin) || build_channel(alt_origin, options)
86
+ # advertised altsvc is the same origin being used, ignore
87
+ return if channel == existing_channel
88
+
89
+ log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
90
+
91
+ # get uninitialized requests
92
+ # incidentally, all requests will be re-routed to the first
93
+ # advertised alt-svc, which incidentally follows the spec.
94
+ existing_channel.purge_pending do |request, args|
95
+ is_idle = request.origin == origin &&
96
+ request.state == :idle &&
97
+ !request.headers.key?("alt-used")
98
+ if is_idle
99
+ log(level: 1) { "#{origin} alt-svc: sending #{request.uri} to #{alt_origin}" }
100
+ channel.send(request, args)
101
+ end
102
+ is_idle
103
+ end
104
+ rescue UnsupportedSchemeError
105
+ altsvc["noop"] = true
106
+ end
107
+
77
108
  def __build_reqs(*args, **options)
78
109
  requests = case args.size
79
110
  when 1
@@ -13,6 +13,7 @@ module HTTPX
13
13
  resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
14
14
  @selector = Selector.new
15
15
  @channels = []
16
+ @connected_channels = 0
16
17
  @resolver = resolver_type.new(self, @options)
17
18
  @resolver.on(:resolve, &method(:on_resolver_channel))
18
19
  @resolver.on(:error, &method(:on_resolver_error))
@@ -32,8 +33,11 @@ module HTTPX
32
33
  monitor.interests = channel.interests
33
34
  end
34
35
  end
35
- rescue TimeoutError,
36
- Errno::ECONNRESET,
36
+ rescue TimeoutError => timeout_error
37
+ @channels.each do |ch|
38
+ ch.handle_timeout_error(timeout_error)
39
+ end
40
+ rescue Errno::ECONNRESET,
37
41
  Errno::ECONNABORTED,
38
42
  Errno::EPIPE => ex
39
43
  @channels.each do |ch|
@@ -50,6 +54,13 @@ module HTTPX
50
54
  def build_channel(uri, **options)
51
55
  channel = Channel.by(uri, @options.merge(options))
52
56
  resolve_channel(channel)
57
+ channel.on(:open) do
58
+ @connected_channels += 1
59
+ @timeout.transition(:open) if @channels.size == @connected_channels
60
+ end
61
+ channel.on(:reset) do
62
+ @timeout.transition(:idle)
63
+ end
53
64
  channel.once(:unreachable) do
54
65
  @resolver.uncache(channel)
55
66
  resolve_channel(channel)
@@ -107,6 +118,7 @@ module HTTPX
107
118
  end
108
119
 
109
120
  def register_channel(channel)
121
+ @timeout.transition(:idle)
110
122
  monitor = @selector.register(channel, :w)
111
123
  monitor.value = channel
112
124
  channel.on(:close) do
@@ -117,12 +129,7 @@ module HTTPX
117
129
  def unregister_channel(channel)
118
130
  @channels.delete(channel)
119
131
  @selector.deregister(channel)
120
- end
121
-
122
- def next_timeout
123
- timeout = @timeout.timeout # force log time
124
- return (@resolver.timeout || timeout) unless @resolver.closed?
125
- timeout
132
+ @connected_channels -= 1
126
133
  end
127
134
 
128
135
  def coalesce_channels(ch1, ch2)
@@ -133,5 +140,11 @@ module HTTPX
133
140
  register_channel(ch2)
134
141
  end
135
142
  end
143
+
144
+ def next_timeout
145
+ timeout = @timeout.timeout
146
+ return (@resolver.timeout || timeout) unless @resolver.closed?
147
+ timeout
148
+ end
136
149
  end
137
150
  end
@@ -3,7 +3,24 @@
3
3
  module HTTPX
4
4
  Error = Class.new(StandardError)
5
5
 
6
- TimeoutError = Class.new(Error)
6
+ UnsupportedSchemeError = Class.new(Error)
7
+
8
+ TimeoutError = Class.new(Error) do
9
+ attr_reader :timeout
10
+
11
+ def initialize(timeout, message)
12
+ @timeout = timeout
13
+ super(message)
14
+ end
15
+
16
+ def to_connection_error
17
+ ex = ConnectTimeoutError.new(@timeout, message)
18
+ ex.set_backtrace(backtrace)
19
+ ex
20
+ end
21
+ end
22
+
23
+ ConnectTimeoutError = Class.new(TimeoutError)
7
24
 
8
25
  ResolveError = Class.new(Error)
9
26
 
@@ -1,52 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless Method.method_defined?(:curry)
4
-
5
- # Backport
6
- #
7
- # Ruby 2.1 and lower implement curry only for Procs.
8
- #
9
- # Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
10
- #
11
- module CurryMethods # :nodoc:
12
- # Backport for the Method#curry method, which is part of ruby core since 2.2 .
3
+ require "uri"
4
+
5
+ module HTTPX
6
+ unless Method.method_defined?(:curry)
7
+
8
+ # Backport
9
+ #
10
+ # Ruby 2.1 and lower implement curry only for Procs.
11
+ #
12
+ # Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
13
13
  #
14
- def curry(*args)
15
- to_proc.curry(*args)
14
+ module CurryMethods # :nodoc:
15
+ # Backport for the Method#curry method, which is part of ruby core since 2.2 .
16
+ #
17
+ def curry(*args)
18
+ to_proc.curry(*args)
19
+ end
16
20
  end
21
+ Method.__send__(:include, CurryMethods)
17
22
  end
18
- Method.__send__(:include, CurryMethods)
19
- end
20
-
21
- unless String.method_defined?(:+@)
22
- # Backport for +"", to initialize unfrozen strings from the string literal.
23
- #
24
- module LiteralStringExtensions
25
- def +@
26
- frozen? ? dup : self
23
+
24
+ unless String.method_defined?(:+@)
25
+ # Backport for +"", to initialize unfrozen strings from the string literal.
26
+ #
27
+ module LiteralStringExtensions
28
+ def +@
29
+ frozen? ? dup : self
30
+ end
27
31
  end
32
+ String.__send__(:include, LiteralStringExtensions)
28
33
  end
29
- String.__send__(:include, LiteralStringExtensions)
30
- end
31
-
32
- unless Numeric.method_defined?(:positive?)
33
- # Ruby 2.3 Backport (Numeric#positive?)
34
- #
35
- module PosMethods
36
- def positive?
37
- self > 0
34
+
35
+ unless Numeric.method_defined?(:positive?)
36
+ # Ruby 2.3 Backport (Numeric#positive?)
37
+ #
38
+ module PosMethods
39
+ def positive?
40
+ self > 0
41
+ end
38
42
  end
43
+ Numeric.__send__(:include, PosMethods)
39
44
  end
40
- Numeric.__send__(:include, PosMethods)
41
- end
42
-
43
- unless Numeric.method_defined?(:negative?)
44
- # Ruby 2.3 Backport (Numeric#negative?)
45
- #
46
- module NegMethods
47
- def negative?
48
- self < 0
45
+
46
+ unless Numeric.method_defined?(:negative?)
47
+ # Ruby 2.3 Backport (Numeric#negative?)
48
+ #
49
+ module NegMethods
50
+ def negative?
51
+ self < 0
52
+ end
53
+ end
54
+ Numeric.__send__(:include, NegMethods)
55
+ end
56
+
57
+ module URIExtensions
58
+ refine URI::Generic do
59
+ def authority
60
+ port_string = port == default_port ? nil : ":#{port}"
61
+ "#{host}#{port_string}"
62
+ end
63
+
64
+ def origin
65
+ "#{scheme}://#{authority}"
66
+ end
67
+
68
+ def altsvc_match?(uri)
69
+ uri = URI.parse(uri)
70
+ self == uri || begin
71
+ case scheme
72
+ when 'h2'
73
+ uri.scheme == "https" &&
74
+ host == uri.host &&
75
+ (port || default_port) == (uri.port || uri.default_port)
76
+ else
77
+ false
78
+ end
79
+ end
80
+ end
49
81
  end
50
82
  end
51
- Numeric.__send__(:include, NegMethods)
52
- end
83
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Parser
5
+ Error = Class.new(Error)
6
+
7
+ class HTTP1
8
+ VERSIONS = %w[1.0 1.1].freeze
9
+
10
+ attr_reader :status_code, :http_version, :headers
11
+
12
+ def initialize(observer, header_separator: ":")
13
+ @observer = observer
14
+ @state = :idle
15
+ @header_separator = header_separator
16
+ @buffer = "".b
17
+ @headers = {}
18
+ end
19
+
20
+ def <<(chunk)
21
+ @buffer << chunk
22
+ parse
23
+ end
24
+
25
+ def reset!
26
+ @state = :idle
27
+ @headers.clear
28
+ @content_length = nil
29
+ @_has_trailers = nil
30
+ end
31
+
32
+ def upgrade?
33
+ @upgrade
34
+ end
35
+
36
+ def upgrade_data
37
+ @buffer
38
+ end
39
+
40
+ private
41
+
42
+ def parse
43
+ state = @state
44
+ case @state
45
+ when :idle
46
+ parse_headline
47
+ when :headers
48
+ parse_headers
49
+ when :trailers
50
+ parse_headers
51
+ when :data
52
+ parse_data
53
+ end
54
+ parse if !@buffer.empty? && state != @state
55
+ end
56
+
57
+ def parse_headline
58
+ idx = @buffer.index("\n")
59
+ return unless idx
60
+ (m = %r{\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
61
+ raise(Error, "wrong head line format")
62
+ version, code, _ = m.captures
63
+ raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version)
64
+ @http_version = version.split(".").map(&:to_i)
65
+ @status_code = code.to_i
66
+ raise(Error, "wrong status code (#{@status_code})") unless (100..599).cover?(@status_code)
67
+ @buffer.slice!(0, idx + 1)
68
+ nextstate(:headers)
69
+ end
70
+
71
+ def parse_headers
72
+ headers = @headers
73
+ while (idx = @buffer.index("\n"))
74
+ line = @buffer.slice!(0, idx + 1).sub(/\s+\z/, "")
75
+ if line.empty?
76
+ case @state
77
+ when :headers
78
+ prepare_data(headers)
79
+ @observer.on_headers(headers)
80
+ return unless @state == :headers
81
+ # state might have been reset
82
+ # in the :headers callback
83
+ nextstate(:data)
84
+ headers.clear
85
+ when :trailers
86
+ @observer.on_trailers(headers)
87
+ headers.clear
88
+ nextstate(:complete)
89
+ else
90
+ raise Error, "wrong header format"
91
+ end
92
+ return
93
+ end
94
+ separator_index = line.index(@header_separator)
95
+ raise Error, "wrong header format" unless separator_index
96
+ key = line[0..separator_index - 1]
97
+ raise Error, "wrong header format" if key.start_with?("\s", "\t")
98
+ key.strip!
99
+ value = line[separator_index + 1..-1]
100
+ value.strip!
101
+ raise Error, "wrong header format" if value.nil?
102
+ (headers[key.downcase] ||= []) << value
103
+ end
104
+ end
105
+
106
+ def parse_data
107
+ if @buffer.respond_to?(:each)
108
+ @buffer.each do |chunk|
109
+ @observer.on_data(chunk)
110
+ end
111
+ elsif @content_length
112
+ data = @buffer.slice!(0, @content_length)
113
+ @content_length -= data.bytesize
114
+ @observer.on_data(data)
115
+ data.clear
116
+ else
117
+ @observer.on_data(@buffer)
118
+ @buffer.clear
119
+ end
120
+ return unless no_more_data?
121
+ @buffer = @buffer.to_s
122
+ if @_has_trailers
123
+ nextstate(:trailers)
124
+ else
125
+ nextstate(:complete)
126
+ end
127
+ end
128
+
129
+ def prepare_data(headers)
130
+ @upgrade = headers.key?("upgrade")
131
+
132
+ @_has_trailers = headers.key?("trailer")
133
+
134
+ if (tr_encodings = headers["transfer-encoding"])
135
+ tr_encodings.reverse_each do |tr_encoding|
136
+ tr_encoding.split(/ *, */).each do |encoding|
137
+ case encoding
138
+ when "chunked"
139
+ @buffer = Transcoder::Chunker::Decoder.new(@buffer, @_has_trailers)
140
+ end
141
+ end
142
+ end
143
+ else
144
+ @content_length = headers["content-length"][0].to_i if headers.key?("content-length")
145
+ end
146
+ end
147
+
148
+ def no_more_data?
149
+ if @content_length
150
+ @content_length <= 0
151
+ elsif @buffer.respond_to?(:finished?)
152
+ @buffer.finished?
153
+ else
154
+ false
155
+ end
156
+ end
157
+
158
+ def nextstate(state)
159
+ @state = state
160
+ case state
161
+ when :headers
162
+ @observer.on_start
163
+ when :complete
164
+ @observer.on_complete
165
+ reset!
166
+ nextstate(:idle) unless @buffer.empty?
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end