httpx 0.2.1 → 0.3.0

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.
@@ -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