httpx 0.7.0 → 0.10.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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +9 -5
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_1_0.md +9 -0
  11. data/doc/release_notes/0_2_0.md +5 -0
  12. data/doc/release_notes/0_2_1.md +16 -0
  13. data/doc/release_notes/0_3_0.md +12 -0
  14. data/doc/release_notes/0_3_1.md +6 -0
  15. data/doc/release_notes/0_4_0.md +51 -0
  16. data/doc/release_notes/0_4_1.md +3 -0
  17. data/doc/release_notes/0_5_0.md +15 -0
  18. data/doc/release_notes/0_5_1.md +14 -0
  19. data/doc/release_notes/0_6_0.md +5 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_6_2.md +6 -0
  22. data/doc/release_notes/0_6_3.md +13 -0
  23. data/doc/release_notes/0_6_4.md +21 -0
  24. data/doc/release_notes/0_6_5.md +22 -0
  25. data/doc/release_notes/0_6_6.md +19 -0
  26. data/doc/release_notes/0_6_7.md +5 -0
  27. data/doc/release_notes/0_7_0.md +46 -0
  28. data/doc/release_notes/0_8_0.md +27 -0
  29. data/doc/release_notes/0_8_1.md +8 -0
  30. data/doc/release_notes/0_8_2.md +7 -0
  31. data/doc/release_notes/0_9_0.md +38 -0
  32. data/lib/httpx.rb +2 -0
  33. data/lib/httpx/adapters/faraday.rb +1 -1
  34. data/lib/httpx/altsvc.rb +18 -2
  35. data/lib/httpx/chainable.rb +9 -8
  36. data/lib/httpx/connection.rb +177 -72
  37. data/lib/httpx/connection/http1.rb +44 -13
  38. data/lib/httpx/connection/http2.rb +77 -34
  39. data/lib/httpx/domain_name.rb +440 -0
  40. data/lib/httpx/errors.rb +1 -0
  41. data/lib/httpx/extensions.rb +23 -3
  42. data/lib/httpx/headers.rb +2 -2
  43. data/lib/httpx/io/ssl.rb +11 -4
  44. data/lib/httpx/io/tcp.rb +16 -5
  45. data/lib/httpx/io/udp.rb +4 -1
  46. data/lib/httpx/loggable.rb +6 -6
  47. data/lib/httpx/options.rb +22 -15
  48. data/lib/httpx/parser/http1.rb +14 -17
  49. data/lib/httpx/plugins/compression.rb +49 -64
  50. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  51. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  52. data/lib/httpx/plugins/compression/gzip.rb +45 -17
  53. data/lib/httpx/plugins/cookies.rb +21 -60
  54. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  55. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  56. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  57. data/lib/httpx/plugins/digest_authentication.rb +2 -0
  58. data/lib/httpx/plugins/expect.rb +12 -1
  59. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  60. data/lib/httpx/plugins/h2c.rb +1 -1
  61. data/lib/httpx/plugins/multipart.rb +0 -8
  62. data/lib/httpx/plugins/persistent.rb +6 -1
  63. data/lib/httpx/plugins/proxy.rb +16 -12
  64. data/lib/httpx/plugins/proxy/http.rb +7 -2
  65. data/lib/httpx/plugins/proxy/socks4.rb +4 -2
  66. data/lib/httpx/plugins/proxy/socks5.rb +5 -1
  67. data/lib/httpx/plugins/push_promise.rb +2 -2
  68. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  69. data/lib/httpx/plugins/retries.rb +13 -6
  70. data/lib/httpx/plugins/stream.rb +109 -13
  71. data/lib/httpx/pool.rb +13 -15
  72. data/lib/httpx/registry.rb +2 -1
  73. data/lib/httpx/request.rb +14 -19
  74. data/lib/httpx/resolver.rb +7 -8
  75. data/lib/httpx/resolver/https.rb +22 -5
  76. data/lib/httpx/resolver/native.rb +27 -33
  77. data/lib/httpx/resolver/options.rb +2 -2
  78. data/lib/httpx/resolver/resolver_mixin.rb +1 -1
  79. data/lib/httpx/response.rb +22 -17
  80. data/lib/httpx/selector.rb +96 -97
  81. data/lib/httpx/session.rb +32 -24
  82. data/lib/httpx/timeout.rb +7 -1
  83. data/lib/httpx/transcoder/chunker.rb +0 -2
  84. data/lib/httpx/transcoder/form.rb +0 -6
  85. data/lib/httpx/transcoder/json.rb +0 -4
  86. data/lib/httpx/utils.rb +45 -0
  87. data/lib/httpx/version.rb +1 -1
  88. data/sig/buffer.rbs +24 -0
  89. data/sig/callbacks.rbs +14 -0
  90. data/sig/chainable.rbs +37 -0
  91. data/sig/connection.rbs +2 -0
  92. data/sig/connection/http2.rbs +4 -0
  93. data/sig/domain_name.rbs +17 -0
  94. data/sig/errors.rbs +3 -0
  95. data/sig/headers.rbs +42 -0
  96. data/sig/httpx.rbs +14 -0
  97. data/sig/loggable.rbs +11 -0
  98. data/sig/missing.rbs +12 -0
  99. data/sig/options.rbs +118 -0
  100. data/sig/parser/http1.rbs +50 -0
  101. data/sig/plugins/authentication.rbs +11 -0
  102. data/sig/plugins/basic_authentication.rbs +13 -0
  103. data/sig/plugins/compression.rbs +55 -0
  104. data/sig/plugins/compression/brotli.rbs +21 -0
  105. data/sig/plugins/compression/deflate.rbs +17 -0
  106. data/sig/plugins/compression/gzip.rbs +29 -0
  107. data/sig/plugins/cookies.rbs +26 -0
  108. data/sig/plugins/cookies/cookie.rbs +50 -0
  109. data/sig/plugins/cookies/jar.rbs +27 -0
  110. data/sig/plugins/digest_authentication.rbs +33 -0
  111. data/sig/plugins/expect.rbs +19 -0
  112. data/sig/plugins/follow_redirects.rbs +37 -0
  113. data/sig/plugins/h2c.rbs +26 -0
  114. data/sig/plugins/multipart.rbs +19 -0
  115. data/sig/plugins/persistent.rbs +17 -0
  116. data/sig/plugins/proxy.rbs +47 -0
  117. data/sig/plugins/proxy/http.rbs +14 -0
  118. data/sig/plugins/proxy/socks4.rbs +33 -0
  119. data/sig/plugins/proxy/socks5.rbs +36 -0
  120. data/sig/plugins/proxy/ssh.rbs +18 -0
  121. data/sig/plugins/push_promise.rbs +22 -0
  122. data/sig/plugins/rate_limiter.rbs +11 -0
  123. data/sig/plugins/retries.rbs +48 -0
  124. data/sig/plugins/stream.rbs +39 -0
  125. data/sig/pool.rbs +2 -0
  126. data/sig/registry.rbs +9 -0
  127. data/sig/request.rbs +61 -0
  128. data/sig/response.rbs +87 -0
  129. data/sig/session.rbs +49 -0
  130. data/sig/test.rbs +9 -0
  131. data/sig/timeout.rbs +29 -0
  132. data/sig/transcoder.rbs +16 -0
  133. data/sig/transcoder/body.rbs +18 -0
  134. data/sig/transcoder/chunker.rbs +32 -0
  135. data/sig/transcoder/form.rbs +16 -0
  136. data/sig/transcoder/json.rbs +14 -0
  137. metadata +120 -21
@@ -31,6 +31,10 @@ module HTTPX
31
31
  end
32
32
  end
33
33
 
34
+ def connecting?
35
+ super || @state == :authenticating || @state == :negotiating
36
+ end
37
+
34
38
  private
35
39
 
36
40
  def transition(nextstate)
@@ -60,7 +64,7 @@ module HTTPX
60
64
 
61
65
  @parser = nil
62
66
  end
63
- log(level: 1, label: "SOCKS5: ") { "#{nextstate}: #{@write_buffer.to_s.inspect}" } unless nextstate == :open
67
+ log(level: 1) { "SOCKS5: #{nextstate}: #{@write_buffer.to_s.inspect}" } unless nextstate == :open
64
68
  super
65
69
  end
66
70
 
@@ -43,9 +43,9 @@ module HTTPX
43
43
  end
44
44
 
45
45
  def __on_promise_request(parser, stream, h)
46
- log(level: 1, label: "#{stream.id}: ") do
46
+ log(level: 1) do
47
47
  # :nocov:
48
- h.map { |k, v| "-> PROMISE HEADER: #{k}: #{v}" }.join("\n")
48
+ h.map { |k, v| "#{stream.id}: -> PROMISE HEADER: #{k}: #{v}" }.join("\n")
49
49
  # :nocov:
50
50
  end
51
51
  headers = @options.headers_class.new(h)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for retrying requests when the request:
7
+ #
8
+ # * is rate limited;
9
+ # * when the server is unavailable (503);
10
+ # * when a 3xx request comes with a "retry-after" value
11
+ #
12
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/RateLimiter
13
+ #
14
+ module RateLimiter
15
+ class << self
16
+ RATE_LIMIT_CODES = [429, 503].freeze
17
+
18
+ def load_dependencies(klass)
19
+ klass.plugin(:retries,
20
+ retry_change_requests: true,
21
+ retry_on: method(:retry_on_rate_limited_response),
22
+ retry_after: method(:retry_after_rate_limit))
23
+ end
24
+
25
+ def retry_on_rate_limited_response(response)
26
+ status = response.status
27
+
28
+ RATE_LIMIT_CODES.include?(status)
29
+ end
30
+
31
+ # Servers send the "Retry-After" header field to indicate how long the
32
+ # user agent ought to wait before making a follow-up request. When
33
+ # sent with a 503 (Service Unavailable) response, Retry-After indicates
34
+ # how long the service is expected to be unavailable to the client.
35
+ # When sent with any 3xx (Redirection) response, Retry-After indicates
36
+ # the minimum time that the user agent is asked to wait before issuing
37
+ # the redirected request.
38
+ #
39
+ def retry_after_rate_limit(_, response)
40
+ retry_after = response.headers["retry-after"]
41
+
42
+ return unless retry_after
43
+
44
+ Utils.parse_retry_after(retry_after)
45
+ end
46
+ end
47
+ end
48
+
49
+ register_plugin :rate_limiter, RateLimiter
50
+ end
51
+ end
@@ -63,20 +63,27 @@ module HTTPX
63
63
  def fetch_response(request, connections, options)
64
64
  response = super
65
65
 
66
- retry_on = options.retry_on
67
-
68
- if response.is_a?(ErrorResponse) &&
66
+ if response &&
69
67
  request.retries.positive? &&
70
68
  __repeatable_request?(request, options) &&
71
- __retryable_error?(response.error) &&
72
- (!retry_on || retry_on.call(response))
69
+ (
70
+ # rubocop:disable Style/MultilineTernaryOperator
71
+ options.retry_on ?
72
+ options.retry_on.call(response) :
73
+ (
74
+ response.is_a?(ErrorResponse) && __retryable_error?(response.error)
75
+ )
76
+ # rubocop:enable Style/MultilineTernaryOperator
77
+ )
78
+ response.close if response.respond_to?(:close)
73
79
  request.retries -= 1
74
80
  log { "failed to get response, #{request.retries} tries to go..." }
75
81
  request.transition(:idle)
76
82
 
77
83
  retry_after = options.retry_after
84
+ retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
85
+
78
86
  if retry_after
79
- retry_after = retry_after.call(request) if retry_after.respond_to?(:call)
80
87
 
81
88
  log { "retrying after #{retry_after} secs..." }
82
89
  pool.after(retry_after) do
@@ -7,27 +7,123 @@ module HTTPX
7
7
  #
8
8
  module Stream
9
9
  module InstanceMethods
10
- def stream
11
- headers("accept" => "text/event-stream",
12
- "cache-control" => "no-cache")
10
+ private
11
+
12
+ def request(*args, stream: false, **options)
13
+ return super(*args, **options) unless stream
14
+
15
+ requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
16
+
17
+ raise Error, "only 1 response at a time is supported for streaming requests" unless requests.size == 1
18
+
19
+ StreamResponse.new(requests.first, self)
13
20
  end
14
21
  end
15
22
 
23
+ module RequestMethods
24
+ attr_accessor :stream
25
+ end
26
+
16
27
  module ResponseMethods
17
- def complete?
18
- super ||
19
- stream? &&
20
- @stream_complete
28
+ def stream
29
+ @request.stream
30
+ end
31
+ end
32
+
33
+ module ResponseBodyMethods
34
+ def initialize(*, **)
35
+ super
36
+ @stream = @response.stream
37
+ end
38
+
39
+ def write(chunk)
40
+ return super unless @stream
41
+
42
+ @stream.on_chunk(chunk.to_s.dup)
43
+ end
44
+
45
+ private
46
+
47
+ def transition(*)
48
+ return if @stream
49
+
50
+ super
51
+ end
52
+ end
53
+
54
+ class StreamResponse
55
+ def initialize(request, session)
56
+ @request = request
57
+ @session = session
58
+ @options = @request.options
59
+ end
60
+
61
+ def each(&block)
62
+ return enum_for(__method__) unless block_given?
63
+
64
+ raise Error, "response already streamed" if @response
65
+
66
+ @request.stream = self
67
+
68
+ begin
69
+ @on_chunk = block
70
+
71
+ response.raise_for_status
72
+ response.close
73
+ ensure
74
+ @on_chunk = nil
75
+ end
76
+ end
77
+
78
+ def each_line
79
+ return enum_for(__method__) unless block_given?
80
+
81
+ line = +""
82
+
83
+ each do |chunk|
84
+ line << chunk
85
+
86
+ while (idx = line.index("\n"))
87
+ yield line.byteslice(0..idx - 1)
88
+
89
+ line = line.byteslice(idx + 1..-1)
90
+ end
91
+ end
92
+ end
93
+
94
+ # This is a ghost method. It's to be used ONLY internally, when processing streams
95
+ def on_chunk(chunk)
96
+ raise NoMethodError unless @on_chunk
97
+
98
+ @on_chunk.call(chunk)
99
+ end
100
+
101
+ # :nocov:
102
+ def inspect
103
+ "#<StreamResponse:#{object_id}>"
104
+ end
105
+ # :nocov:
106
+
107
+ def to_s
108
+ response.to_s
109
+ end
110
+
111
+ private
112
+
113
+ def response
114
+ @response ||= @session.__send__(:send_requests, @request, @options).first
21
115
  end
22
116
 
23
- def stream?
24
- @headers["content-type"].start_with?("text/event-stream")
117
+ def respond_to_missing?(*args)
118
+ @options.response_class.respond_to?(*args) || super
25
119
  end
26
120
 
27
- def <<(data)
28
- res = super
29
- @stream_complete = true if String(data).end_with?("\n\n")
30
- res
121
+ def method_missing(meth, *args, &block)
122
+ if @options.response_class.public_method_defined?(meth)
123
+ response.__send__(meth, *args, &block)
124
+ else
125
+ super
126
+ end
31
127
  end
32
128
  end
33
129
  end
@@ -14,7 +14,7 @@ module HTTPX
14
14
 
15
15
  def initialize
16
16
  @resolvers = {}
17
- @_resolver_monitors = {}
17
+ @_resolver_ios = {}
18
18
  @timers = Timers::Group.new
19
19
  @selector = Selector.new
20
20
  @connections = []
@@ -28,28 +28,31 @@ module HTTPX
28
28
  def next_tick
29
29
  catch(:jump_tick) do
30
30
  timeout = [next_timeout, @timers.wait_interval].compact.min
31
- if timeout.negative?
31
+ if timeout && timeout.negative?
32
32
  @timers.fire
33
33
  throw(:jump_tick)
34
34
  end
35
35
 
36
- @selector.select(timeout) do |monitor|
37
- monitor.io.call
38
- monitor.interests = monitor.io.interests
39
- end
36
+ @selector.select(timeout, &:call)
37
+
40
38
  @timers.fire
41
39
  end
42
40
  rescue StandardError => e
43
41
  @connections.each do |connection|
44
42
  connection.emit(:error, e)
45
43
  end
44
+ rescue Exception # rubocop:disable Lint/RescueException
45
+ @connections.each(&:reset)
46
+ raise
46
47
  end
47
48
 
48
49
  def close(connections = @connections)
50
+ return if connections.empty?
51
+
49
52
  @timers.cancel
50
53
  connections = connections.reject(&:inflight?)
51
54
  connections.each(&:close)
52
- next_tick until connections.none? { |c| @connections.include?(c) }
55
+ next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
53
56
  @resolvers.each_value do |resolver|
54
57
  resolver.close unless resolver.closed?
55
58
  end if @connections.empty?
@@ -86,7 +89,7 @@ module HTTPX
86
89
  resolver << connection
87
90
  return if resolver.empty?
88
91
 
89
- @_resolver_monitors[resolver] ||= @selector.register(resolver, :w)
92
+ @_resolver_ios[resolver] ||= @selector.register(resolver)
90
93
  end
91
94
 
92
95
  def on_resolver_connection(connection)
@@ -118,8 +121,7 @@ module HTTPX
118
121
  @resolvers.delete(resolver_type)
119
122
 
120
123
  @selector.deregister(resolver)
121
- monitor = @_resolver_monitors.delete(resolver)
122
- monitor.close if monitor
124
+ @_resolver_ios.delete(resolver)
123
125
  resolver.close unless resolver.closed?
124
126
  end
125
127
 
@@ -128,10 +130,8 @@ module HTTPX
128
130
  # if open, an IO was passed upstream, therefore
129
131
  # consider it connected already.
130
132
  @connected_connections += 1
131
- @selector.register(connection, :rw)
132
- else
133
- @selector.register(connection, :w)
134
133
  end
134
+ @selector.register(connection)
135
135
  connection.on(:close) do
136
136
  unregister_connection(connection)
137
137
  end
@@ -168,7 +168,6 @@ module HTTPX
168
168
  resolver.on(:error, &method(:on_resolver_error))
169
169
  resolver.on(:close) { on_resolver_close(resolver) }
170
170
  resolver
171
- # rubocop: disable Layout/RescueEnsureAlignment
172
171
  rescue ArgumentError
173
172
  # this block is here because of an error which happens on CI from time to time
174
173
  warn "tried resolver: #{resolver_type}"
@@ -176,7 +175,6 @@ module HTTPX
176
175
  warn "new: #{resolver_type.method(:new).source_location}"
177
176
  raise
178
177
  end
179
- # rubocop: enable Layout/RescueEnsureAlignment
180
178
  end
181
179
  end
182
180
  end
@@ -64,7 +64,8 @@ module HTTPX
64
64
 
65
65
  case handler
66
66
  when Symbol, String
67
- const_get(handler)
67
+ obj = const_get(handler)
68
+ @registry[tag] = obj
68
69
  else
69
70
  handler
70
71
  end
@@ -33,11 +33,7 @@ module HTTPX
33
33
 
34
34
  USER_AGENT = "httpx.rb/#{VERSION}"
35
35
 
36
- attr_reader :verb, :uri, :headers, :body, :state
37
-
38
- attr_reader :options, :response
39
-
40
- def_delegator :@body, :<<
36
+ attr_reader :verb, :uri, :headers, :body, :state, :options, :response
41
37
 
42
38
  def_delegator :@body, :empty?
43
39
 
@@ -45,7 +41,7 @@ module HTTPX
45
41
 
46
42
  def initialize(verb, uri, options = {})
47
43
  @verb = verb.to_s.downcase.to_sym
48
- @uri = URI(uri.to_s)
44
+ @uri = Utils.uri(uri)
49
45
  @options = Options.new(options)
50
46
 
51
47
  raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
@@ -58,17 +54,21 @@ module HTTPX
58
54
  @state = :idle
59
55
  end
60
56
 
61
- # :nocov:
57
+ def interests
58
+ return :r if @state == :done || @state == :expect
59
+
60
+ :w
61
+ end
62
+
62
63
  if RUBY_VERSION < "2.2"
63
- # rubocop: disable Lint/UriEscapeUnescape:
64
+ URIParser = URI::DEFAULT_PARSER
65
+
64
66
  def initialize_with_escape(verb, uri, options = {})
65
- initialize_without_escape(verb, URI.escape(uri.to_s), options)
67
+ initialize_without_escape(verb, URIParser.escape(uri.to_s), options)
66
68
  end
67
69
  alias_method :initialize_without_escape, :initialize
68
70
  alias_method :initialize, :initialize_with_escape
69
- # rubocop: enable Lint/UriEscapeUnescape:
70
71
  end
71
- # :nocov:
72
72
 
73
73
  def merge_headers(h)
74
74
  @headers = @headers.merge(h)
@@ -174,19 +174,13 @@ module HTTPX
174
174
  return true if @body.nil?
175
175
  return false if chunked?
176
176
 
177
- bytesize.zero?
177
+ @body.bytesize.zero?
178
178
  end
179
179
 
180
180
  def bytesize
181
181
  return 0 if @body.nil?
182
182
 
183
- if @body.respond_to?(:bytesize)
184
- @body.bytesize
185
- elsif @body.respond_to?(:size)
186
- @body.size
187
- else
188
- raise Error, "cannot determine size of body: #{@body.inspect}"
189
- end
183
+ @body.bytesize
190
184
  end
191
185
 
192
186
  def stream(body)
@@ -219,6 +213,7 @@ module HTTPX
219
213
  case nextstate
220
214
  when :idle
221
215
  @response = nil
216
+ @drainer = nil
222
217
  when :headers
223
218
  return unless @state == :idle
224
219
  when :body
@@ -1,19 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "resolv"
4
+ require "httpx/resolver/resolver_mixin"
5
+ require "httpx/resolver/system"
6
+ require "httpx/resolver/native"
7
+ require "httpx/resolver/https"
4
8
 
5
9
  module HTTPX
6
10
  module Resolver
7
- autoload :ResolverMixin, "httpx/resolver/resolver_mixin"
8
- autoload :System, "httpx/resolver/system"
9
- autoload :Native, "httpx/resolver/native"
10
- autoload :HTTPS, "httpx/resolver/https"
11
-
12
11
  extend Registry
13
12
 
14
- register :system, :System
15
- register :native, :Native
16
- register :https, :HTTPS
13
+ register :system, System
14
+ register :native, Native
15
+ register :https, HTTPS
17
16
 
18
17
  @lookup_mutex = Mutex.new
19
18
  @lookups = Hash.new { |h, k| h[k] = [] }