httpx 1.7.5 → 1.7.7

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_4.md +1 -1
  3. data/doc/release_notes/1_7_6.md +24 -0
  4. data/doc/release_notes/1_7_7.md +17 -0
  5. data/lib/httpx/adapters/datadog.rb +14 -1
  6. data/lib/httpx/adapters/faraday.rb +0 -10
  7. data/lib/httpx/adapters/webmock.rb +1 -1
  8. data/lib/httpx/altsvc.rb +4 -2
  9. data/lib/httpx/connection/http1.rb +52 -43
  10. data/lib/httpx/connection/http2.rb +23 -16
  11. data/lib/httpx/connection.rb +80 -43
  12. data/lib/httpx/io/ssl.rb +24 -12
  13. data/lib/httpx/io/tcp.rb +18 -12
  14. data/lib/httpx/io/unix.rb +13 -9
  15. data/lib/httpx/loggable.rb +1 -1
  16. data/lib/httpx/options.rb +23 -7
  17. data/lib/httpx/parser/http1.rb +14 -5
  18. data/lib/httpx/plugins/auth/digest.rb +6 -0
  19. data/lib/httpx/plugins/auth.rb +23 -9
  20. data/lib/httpx/plugins/circuit_breaker/circuit.rb +1 -0
  21. data/lib/httpx/plugins/cookies/cookie.rb +0 -1
  22. data/lib/httpx/plugins/digest_auth.rb +3 -1
  23. data/lib/httpx/plugins/follow_redirects.rb +13 -1
  24. data/lib/httpx/plugins/h2c.rb +2 -12
  25. data/lib/httpx/plugins/proxy/http.rb +1 -1
  26. data/lib/httpx/plugins/proxy.rb +1 -1
  27. data/lib/httpx/plugins/response_cache.rb +11 -4
  28. data/lib/httpx/plugins/retries.rb +6 -6
  29. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  30. data/lib/httpx/plugins/tracing.rb +1 -6
  31. data/lib/httpx/plugins/upgrade/h2.rb +1 -11
  32. data/lib/httpx/plugins/upgrade.rb +17 -17
  33. data/lib/httpx/pool.rb +7 -9
  34. data/lib/httpx/request.rb +28 -3
  35. data/lib/httpx/resolver/native.rb +1 -1
  36. data/lib/httpx/response.rb +5 -1
  37. data/lib/httpx/selector.rb +18 -10
  38. data/lib/httpx/session.rb +32 -24
  39. data/lib/httpx/version.rb +1 -1
  40. data/sig/altsvc.rbs +2 -0
  41. data/sig/connection/http1.rbs +4 -2
  42. data/sig/connection/http2.rbs +1 -1
  43. data/sig/connection.rbs +9 -2
  44. data/sig/io/ssl.rbs +1 -0
  45. data/sig/io/tcp.rbs +2 -2
  46. data/sig/loggable.rbs +1 -1
  47. data/sig/options.rbs +8 -3
  48. data/sig/parser/http1.rbs +1 -1
  49. data/sig/plugins/auth/basic.rbs +1 -1
  50. data/sig/plugins/auth/digest.rbs +1 -1
  51. data/sig/plugins/auth/ntlm.rbs +2 -0
  52. data/sig/plugins/auth.rbs +5 -2
  53. data/sig/plugins/follow_redirects.rbs +1 -1
  54. data/sig/plugins/proxy.rbs +1 -0
  55. data/sig/plugins/response_cache.rbs +2 -0
  56. data/sig/plugins/retries.rbs +1 -1
  57. data/sig/plugins/tracing.rbs +1 -1
  58. data/sig/pool.rbs +1 -1
  59. data/sig/request.rbs +6 -0
  60. data/sig/session.rbs +0 -2
  61. metadata +5 -1
@@ -62,6 +62,7 @@ module HTTPX
62
62
  @pending = []
63
63
  @inflight = 0
64
64
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
65
+ @no_more_requests_counter = 0
65
66
 
66
67
  if @options.io
67
68
  # if there's an already open IO, get its
@@ -106,7 +107,7 @@ module HTTPX
106
107
  # origin came from an ORIGIN frame, we're going to verify the hostname with the
107
108
  # SSL certificate
108
109
  (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) &&
109
- @options == options
110
+ @options.connection_options_match?(options)
110
111
  end
111
112
 
112
113
  def mergeable?(connection)
@@ -117,7 +118,7 @@ module HTTPX
117
118
  (
118
119
  (open? && @origin == connection.origin) ||
119
120
  !(@io.addresses & (connection.addresses || [])).empty?
120
- ) && @options == connection.options
121
+ ) && @options.connection_options_match?(connection.options)
121
122
  end
122
123
 
123
124
  # coalesces +self+ into +connection+.
@@ -287,6 +288,10 @@ module HTTPX
287
288
  def reset
288
289
  return if @state == :closing || @state == :closed
289
290
 
291
+ # do not reset a connection which may have restarted back to :idle, such when the parser resets
292
+ # (example: HTTP/1 parser disabling pipelining)
293
+ return if @state == :idle && @pending.any?
294
+
290
295
  parser = @parser
291
296
 
292
297
  if parser && parser.respond_to?(:max_concurrent_requests)
@@ -313,8 +318,8 @@ module HTTPX
313
318
  log(level: 3) { "keep alive timeout expired, pinging connection..." }
314
319
  @pending << request
315
320
  transition(:active) if @state == :inactive
316
- parser.ping
317
321
  request.ping!
322
+ ping
318
323
  return
319
324
  end
320
325
 
@@ -455,11 +460,11 @@ module HTTPX
455
460
  #
456
461
  # this condition takes into account:
457
462
  #
458
- # * the number of inflight requests
459
463
  # * the number of pending requests
464
+ # * the number of inflight requests
460
465
  # * whether the write buffer has bytes (i.e. for close handshake)
461
466
  if @pending.empty? && @inflight.zero? && @write_buffer.empty?
462
- log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
467
+ no_more_requests_loop_check if @parser && @parser.pending.any?
463
468
 
464
469
  # terminate if an altsvc connection has been established
465
470
  terminate if @altsvc_connection
@@ -509,7 +514,7 @@ module HTTPX
509
514
 
510
515
  # exit #consume altogether if all outstanding requests have been dealt with
511
516
  if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
512
- log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
517
+ no_more_requests_loop_check if @parser && @parser.pending.any?
513
518
 
514
519
  # terminate if an altsvc connection has been established
515
520
  terminate if @altsvc_connection
@@ -635,15 +640,16 @@ module HTTPX
635
640
  build_altsvc_connection(alt_origin, origin, alt_params)
636
641
  end
637
642
  @response_received_at = Utils.now
643
+ @no_more_requests_counter = 0
638
644
  @inflight -= 1
639
645
  response.finish!
640
- request.emit(:response, response)
646
+ request.emit_response(response)
641
647
  end
642
648
  parser.on(:altsvc) do |alt_origin, origin, alt_params|
643
649
  build_altsvc_connection(alt_origin, origin, alt_params)
644
650
  end
645
651
 
646
- parser.on(:pong, &method(:send_pending))
652
+ parser.on(:pong, &method(:pong))
647
653
 
648
654
  parser.on(:promise) do |request, stream|
649
655
  request.emit(:promise, parser, stream)
@@ -701,7 +707,7 @@ module HTTPX
701
707
  @inflight -= 1
702
708
  response = ErrorResponse.new(request, error)
703
709
  request.response = response
704
- request.emit(:response, response)
710
+ request.emit_response(response)
705
711
  end
706
712
  end
707
713
 
@@ -755,16 +761,6 @@ module HTTPX
755
761
  return if @inflight.positive? || @parser.waiting_for_ping?
756
762
  when :closing
757
763
  return unless connecting? || @state == :open
758
-
759
- unless @write_buffer.empty?
760
- # preset state before handshake, as error callbacks
761
- # may take it back here.
762
- @state = nextstate
763
- # handshakes, try sending
764
- consume
765
- @write_buffer.clear
766
- return
767
- end
768
764
  when :closed
769
765
  return unless @state == :closing
770
766
  return unless @write_buffer.empty?
@@ -790,6 +786,12 @@ module HTTPX
790
786
  case nextstate
791
787
  when :inactive
792
788
  disconnect
789
+ when :closing
790
+ return if @write_buffer.empty?
791
+
792
+ # try flushing termination handshakes
793
+ consume
794
+ @write_buffer.clear
793
795
  when :closed
794
796
  # TODO: should this raise an error instead?
795
797
  return unless @pending.empty?
@@ -813,16 +815,18 @@ module HTTPX
813
815
  end
814
816
 
815
817
  def close_sibling
816
- return unless @sibling
818
+ sibling = @sibling
817
819
 
818
- if @sibling.io_connected?
820
+ return unless sibling
821
+
822
+ if sibling.io_connected?
819
823
  reset
820
824
  # TODO: transition connection to closed
821
825
  end
822
826
 
823
- unless @sibling.state == :closed
824
- merge(@sibling) unless @main_sibling
825
- @sibling.force_reset(true)
827
+ unless sibling.state == :closed
828
+ merge(sibling) unless @main_sibling
829
+ sibling.force_reset(true)
826
830
  end
827
831
 
828
832
  @sibling = nil
@@ -902,28 +906,61 @@ module HTTPX
902
906
  end
903
907
  end
904
908
 
909
+ def ping
910
+ return if parser.waiting_for_ping?
911
+
912
+ parser.ping
913
+ call
914
+ end
915
+
916
+ def pong
917
+ @response_received_at = Utils.now
918
+ @no_more_requests_counter = 0
919
+ send_pending
920
+ end
921
+
922
+ def no_more_requests_loop_check
923
+ log(level: 3) { "NO MORE REQUESTS..." }
924
+ @no_more_requests_counter += 1
925
+
926
+ return if @no_more_requests_counter < 50
927
+
928
+ raise Error, "connection corrupted, aborted after looping for a while, " \
929
+ "please report this https://gitlab.com/os85/httpx/-/work_items " \
930
+ "along with debug logs"
931
+ end
932
+
905
933
  # recover internal state and emit all relevant error responses when +error+ was raised.
906
934
  # this takes an optiona +request+ which may have already been handled and can be opted out
907
935
  # in the state recovery process.
908
936
  def handle_error(error, request = nil)
909
- parser.handle_error(error, request) if @parser && @parser.respond_to?(:handle_error)
910
- while (req = @pending.shift)
911
- next if request && req == request
937
+ if request
938
+ @inflight -= 1
939
+ response = ErrorResponse.new(request, error)
940
+ request.response = response
941
+ request.emit_response(response)
942
+ end
912
943
 
913
- response = ErrorResponse.new(req, error)
914
- req.response = response
915
- req.emit(:response, response)
944
+ pending = @pending
945
+ if (parser = @parser) && parser.respond_to?(:handle_error)
946
+ # parser.handle_error may disconnect the connection
947
+ pending = @pending.dup
948
+ @pending = []
949
+
950
+ parser.handle_error(error, request)
916
951
  end
917
952
 
918
- return unless request
953
+ while (req = pending.shift)
954
+ next if request && req == request
919
955
 
920
- @inflight -= 1
921
- response = ErrorResponse.new(request, error)
922
- request.response = response
923
- request.emit(:response, response)
956
+ resp = ErrorResponse.new(req, error)
957
+ req.response = resp
958
+ req.emit_response(resp)
959
+ end
924
960
  end
925
961
 
926
962
  def set_request_timeouts(request)
963
+ request.connection = self
927
964
  set_request_write_timeout(request)
928
965
  set_request_read_timeout(request)
929
966
  set_request_request_timeout(request)
@@ -959,29 +996,29 @@ module HTTPX
959
996
  end
960
997
  end
961
998
 
962
- def write_timeout_callback(request, write_timeout)
999
+ def write_timeout_callback(request, timeout)
963
1000
  return if request.state == :done
964
1001
 
965
1002
  @write_buffer.clear
966
- error = WriteTimeoutError.new(request, nil, write_timeout)
1003
+ error = WriteTimeoutError.new(request, nil, timeout)
967
1004
 
968
- on_error(error, request)
1005
+ request.handle_error(error)
969
1006
  end
970
1007
 
971
- def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
1008
+ def read_timeout_callback(request, timeout, error_type = ReadTimeoutError)
972
1009
  response = request.response
973
1010
 
974
1011
  return if response && response.finished?
975
1012
 
976
1013
  @write_buffer.clear
977
- error = error_type.new(request, request.response, read_timeout)
1014
+ error = error_type.new(request, response, timeout)
978
1015
 
979
- on_error(error, request)
1016
+ request.handle_error(error)
980
1017
  end
981
1018
 
982
1019
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
983
1020
  request.set_timeout_callback(start_event) do
984
- unless @current_selector
1021
+ unless (selector = @current_selector)
985
1022
  raise Error, "request has been resend to an out-of-session connection, and this " \
986
1023
  "should never happen!!! Please report this error! " \
987
1024
  "(state:#{@state}, " \
@@ -992,7 +1029,7 @@ module HTTPX
992
1029
  "coalesced?:#{coalesced?})"
993
1030
  end
994
1031
 
995
- timer = @current_selector.after(timeout, callback)
1032
+ timer = selector.after(timeout, callback)
996
1033
  request.active_timeouts << label
997
1034
 
998
1035
  Array(finish_events).each do |event|
data/lib/httpx/io/ssl.rb CHANGED
@@ -51,6 +51,7 @@ module HTTPX
51
51
  end
52
52
 
53
53
  def protocol
54
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
54
55
  @io.alpn_protocol || super
55
56
  rescue StandardError
56
57
  super
@@ -60,6 +61,7 @@ module HTTPX
60
61
  # in jruby, alpn_protocol may return ""
61
62
  # https://github.com/jruby/jruby-openssl/issues/287
62
63
  def protocol
64
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
63
65
  proto = @io.alpn_protocol
64
66
 
65
67
  return super if proto.nil? || proto.empty?
@@ -76,9 +78,10 @@ module HTTPX
76
78
 
77
79
  def verify_hostname(host)
78
80
  return false if @ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
79
- return false if !@io.respond_to?(:peer_cert) || @io.peer_cert.nil?
81
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
82
+ return false if !@io.respond_to?(:peer_cert) || (peer_cert = @io.peer_cert).nil?
80
83
 
81
- OpenSSL::SSL.verify_certificate_identity(@io.peer_cert, host)
84
+ OpenSSL::SSL.verify_certificate_identity(peer_cert, host)
82
85
  end
83
86
 
84
87
  def connected?
@@ -86,7 +89,9 @@ module HTTPX
86
89
  end
87
90
 
88
91
  def ssl_session_expired?
89
- @ssl_session.nil? || Process.clock_gettime(Process::CLOCK_REALTIME) >= (@ssl_session.time.to_f + @ssl_session.timeout)
92
+ ssl_session = @ssl_session
93
+
94
+ ssl_session.nil? || Process.clock_gettime(Process::CLOCK_REALTIME) >= (ssl_session.time.to_f + ssl_session.timeout)
90
95
  end
91
96
 
92
97
  def connect
@@ -97,6 +102,8 @@ module HTTPX
97
102
  return unless @state == :connected
98
103
  end
99
104
 
105
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
106
+
100
107
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
101
108
  if (hostname_is_ip = (@ip == @sni_hostname)) && @ctx.verify_hostname
102
109
  # IPv6 address would be "[::1]", must turn to "0000:0000:0000:0000:0000:0000:0000:0001" for cert SAN check
@@ -105,16 +112,19 @@ module HTTPX
105
112
  @ctx.verify_hostname = false
106
113
  end
107
114
 
108
- @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
115
+ ssl = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
109
116
 
110
- @io.hostname = @sni_hostname unless hostname_is_ip
111
- @io.session = @ssl_session unless ssl_session_expired?
112
- @io.sync_close = true
117
+ ssl.hostname = @sni_hostname unless hostname_is_ip
118
+ ssl.session = @ssl_session unless ssl_session_expired?
119
+ ssl.sync_close = true
120
+
121
+ @io = ssl
113
122
  end
114
123
  try_ssl_connect
115
124
  end
116
125
 
117
126
  def try_ssl_connect
127
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
118
128
  ret = @io.connect_nonblock(exception: false)
119
129
  log(level: 3, color: :cyan) { "TLS CONNECT: #{ret}..." }
120
130
  case ret
@@ -147,16 +157,18 @@ module HTTPX
147
157
  def log_transition_state(nextstate)
148
158
  return super unless nextstate == :negotiated
149
159
 
150
- server_cert = @io.peer_cert
160
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
161
+
162
+ server_cert = @io.peer_cert #: OpenSSL::X509::Certificate
151
163
 
152
164
  "#{super}\n\n" \
153
165
  "SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
154
166
  "ALPN, server accepted to use #{protocol}\n" \
155
167
  "Server certificate:\n " \
156
- "subject: #{server_cert.subject}\n " \
157
- "start date: #{server_cert.not_before}\n " \
158
- "expire date: #{server_cert.not_after}\n " \
159
- "issuer: #{server_cert.issuer}\n " \
168
+ "subject: #{log_redact(server_cert.subject)}\n " \
169
+ "start date: #{log_redact(server_cert.not_before)}\n " \
170
+ "expire date: #{log_redact(server_cert.not_after)}\n " \
171
+ "issuer: #{log_redact(server_cert.issuer)}\n " \
160
172
  "SSL certificate verify ok."
161
173
  end
162
174
  end
data/lib/httpx/io/tcp.rb CHANGED
@@ -23,18 +23,21 @@ module HTTPX
23
23
  @fallback_protocol = @options.fallback_protocol
24
24
  @port = origin.port
25
25
  @interests = :w
26
- if @options.io
27
- @io = case @options.io
28
- when Hash
29
- @options.io[origin.authority]
30
- else
31
- @options.io
32
- end
33
- raise Error, "Given IO objects do not match the request authority" unless @io
34
-
35
- _, _, _, ip = @io.addr
36
- @ip = Resolver::Entry.new(ip)
37
- @addresses << @ip
26
+ if (io = @options.io)
27
+ io =
28
+ case io
29
+ when Hash
30
+ io[origin.authority]
31
+ else
32
+ io
33
+ end
34
+ raise Error, "Given IO objects do not match the request authority" unless io
35
+
36
+ # @type var io: TCPSocket | OpenSSL::SSL::SSLSocket
37
+
38
+ _, _, _, ip = io.addr
39
+ @io = io
40
+ @addresses << (@ip = Resolver::Entry.new(ip))
38
41
  @keep_open = true
39
42
  @state = :connected
40
43
  else
@@ -183,6 +186,9 @@ module HTTPX
183
186
 
184
187
  begin
185
188
  @io.close
189
+ rescue StandardError => e
190
+ log { "error closing socket" }
191
+ log { e.full_message(highlight: false) }
186
192
  ensure
187
193
  transition(:closed)
188
194
  end
data/lib/httpx/io/unix.rb CHANGED
@@ -14,16 +14,20 @@ module HTTPX
14
14
  @state = :idle
15
15
  @options = options
16
16
  @fallback_protocol = @options.fallback_protocol
17
- if @options.io
18
- @io = case @options.io
19
- when Hash
20
- @options.io[origin.authority]
21
- else
22
- @options.io
23
- end
24
- raise Error, "Given IO objects do not match the request authority" unless @io
17
+ if (io = @options.io)
18
+ io =
19
+ case io
20
+ when Hash
21
+ io[origin.authority]
22
+ else
23
+ io
24
+ end
25
+ raise Error, "Given IO objects do not match the request authority" unless io
26
+
27
+ # @type var io: UNIXSocket
25
28
 
26
- @path = @io.path
29
+ _, @path = io.addr
30
+ @io = io
27
31
  @keep_open = true
28
32
  @state = :connected
29
33
  elsif path
@@ -57,7 +57,7 @@ module HTTPX
57
57
  log_redact(text, @options.debug_redact == :body)
58
58
  end
59
59
 
60
- def log_redact(text, should_redact)
60
+ def log_redact(text, should_redact = nil)
61
61
  should_redact ||= @options.debug_redact == true
62
62
 
63
63
  return text.to_s unless should_redact
data/lib/httpx/options.rb CHANGED
@@ -202,16 +202,19 @@ module HTTPX
202
202
 
203
203
  REQUEST_BODY_IVARS = %i[@headers].freeze
204
204
 
205
- def ==(other)
206
- super || options_equals?(other)
207
- end
205
+ # checks whether +other+ matches the same connection-level options
206
+ def connection_options_match?(other, ignore_ivars = nil)
207
+ return true if self == other
208
208
 
209
- # checks whether +other+ is equal by comparing the session options
210
- def options_equals?(other, ignore_ivars = REQUEST_BODY_IVARS)
211
209
  # headers and other request options do not play a role, as they are
212
210
  # relevant only for the request.
213
- ivars = instance_variables - ignore_ivars
214
- other_ivars = other.instance_variables - ignore_ivars
211
+ ivars = instance_variables
212
+ ivars.reject! { |iv| REQUEST_BODY_IVARS.include?(iv) }
213
+ ivars.reject! { |iv| ignore_ivars.include?(iv) } if ignore_ivars
214
+
215
+ other_ivars = other.instance_variables
216
+ other_ivars.reject! { |iv| REQUEST_BODY_IVARS.include?(iv) }
217
+ other_ivars.reject! { |iv| ignore_ivars.include?(iv) } if ignore_ivars
215
218
 
216
219
  return false if ivars.size != other_ivars.size
217
220
 
@@ -222,6 +225,19 @@ module HTTPX
222
225
  end
223
226
  end
224
227
 
228
+ RESOLVER_IVARS = %i[
229
+ @resolver_class @resolver_cache @resolver_options
230
+ @resolver_native_class @resolver_system_class @resolver_https_class
231
+ ].freeze
232
+
233
+ # checks whether +other+ matches the same resolver-level options
234
+ def resolver_options_match?(other)
235
+ self == other ||
236
+ RESOLVER_IVARS.all? do |ivar|
237
+ instance_variable_get(ivar) == other.instance_variable_get(ivar)
238
+ end
239
+ end
240
+
225
241
  # returns a HTTPX::Options instance resulting of the merging of +other+ with self.
226
242
  # it may return self if +other+ is self or equal to self.
227
243
  def merge(other)
@@ -14,6 +14,8 @@ module HTTPX
14
14
  @state = :idle
15
15
  @buffer = "".b
16
16
  @headers = {}
17
+ @content_length = nil
18
+ @_has_trailers = @upgrade = false
17
19
  end
18
20
 
19
21
  def <<(chunk)
@@ -25,8 +27,8 @@ module HTTPX
25
27
  @state = :idle
26
28
  @headers = {}
27
29
  @content_length = nil
28
- @_has_trailers = nil
29
- @buffer.clear
30
+ @_has_trailers = @upgrade = false
31
+ @buffer = @buffer.to_s
30
32
  end
31
33
 
32
34
  def upgrade?
@@ -34,7 +36,7 @@ module HTTPX
34
36
  end
35
37
 
36
38
  def upgrade_data
37
- @buffer
39
+ @buffer.to_s
38
40
  end
39
41
 
40
42
  private
@@ -55,6 +57,7 @@ module HTTPX
55
57
  end
56
58
 
57
59
  def parse_headline
60
+ #: @type ivar @buffer: String
58
61
  idx = @buffer.index("\n")
59
62
  return unless idx
60
63
 
@@ -75,6 +78,8 @@ module HTTPX
75
78
  headers = @headers
76
79
  buffer = @buffer
77
80
 
81
+ #: @type var buffer: String
82
+
78
83
  while (idx = buffer.index("\n"))
79
84
  # @type var line: String
80
85
  line = buffer.byteslice(0..idx)
@@ -118,17 +123,20 @@ module HTTPX
118
123
 
119
124
  def parse_data
120
125
  if @buffer.respond_to?(:each)
126
+ # @type ivar @buffer: Transcoder::Chunker::Decoder
121
127
  @buffer.each do |chunk|
122
128
  @observer.on_data(chunk)
123
129
  end
124
130
  elsif @content_length
125
- # @type var data: String
131
+ # @type ivar @buffer: String
126
132
  data = @buffer.byteslice(0, @content_length)
133
+ # @type var data: String
127
134
  @buffer = @buffer.byteslice(@content_length..-1) || "".b
128
135
  @content_length -= data.bytesize
129
136
  @observer.on_data(data)
130
137
  data.clear
131
138
  else
139
+ # @type ivar @buffer: String
132
140
  @observer.on_data(@buffer)
133
141
  @buffer.clear
134
142
  end
@@ -152,7 +160,7 @@ module HTTPX
152
160
  tr_encoding.split(/ *, */).each do |encoding|
153
161
  case encoding
154
162
  when "chunked"
155
- @buffer = Transcoder::Chunker::Decoder.new(@buffer, @_has_trailers)
163
+ @buffer = Transcoder::Chunker::Decoder.new(@buffer.to_s, @_has_trailers)
156
164
  end
157
165
  end
158
166
  end
@@ -165,6 +173,7 @@ module HTTPX
165
173
  if @content_length
166
174
  @content_length <= 0
167
175
  elsif @buffer.respond_to?(:finished?)
176
+ # @type ivar @buffer: Transcoder::Chunker::Decoder
168
177
  @buffer.finished?
169
178
  else
170
179
  false
@@ -24,6 +24,11 @@ module HTTPX
24
24
 
25
25
  def authenticate(request, authenticate)
26
26
  "Digest #{generate_header(request.verb, request.path, authenticate)}"
27
+ rescue StandardError => e
28
+ response = ErrorResponse.new(request, e)
29
+ request.response = response
30
+ request.emit_response(response)
31
+ nil
27
32
  end
28
33
 
29
34
  private
@@ -77,6 +82,7 @@ module HTTPX
77
82
 
78
83
  if params["algorithm"] =~ /(.*?)(-sess)?$/
79
84
  alg = Regexp.last_match(1)
85
+ raise_format_error unless alg
80
86
  algorithm = ::Digest.const_get(alg)
81
87
  raise Error, "unknown algorithm \"#{alg}\"" unless algorithm
82
88
 
@@ -44,6 +44,7 @@ module HTTPX
44
44
  super
45
45
 
46
46
  @auth_header_value = nil
47
+ @auth_header_value_mtx = Thread::Mutex.new
47
48
  @skip_auth_header_value = false
48
49
  end
49
50
 
@@ -63,7 +64,9 @@ module HTTPX
63
64
  end
64
65
 
65
66
  def reset_auth_header_value!
66
- @auth_header_value = nil
67
+ @auth_header_value_mtx.synchronize do
68
+ @auth_header_value = nil
69
+ end
67
70
  end
68
71
 
69
72
  private
@@ -71,9 +74,11 @@ module HTTPX
71
74
  def send_request(request, *)
72
75
  return super if @skip_auth_header_value || request.authorized?
73
76
 
74
- @auth_header_value ||= generate_auth_token
77
+ auth_header_value = @auth_header_value_mtx.synchronize do
78
+ @auth_header_value ||= generate_auth_token
79
+ end
75
80
 
76
- request.authorize(@auth_header_value) if @auth_header_value
81
+ request.authorize(auth_header_value) if auth_header_value
77
82
 
78
83
  super
79
84
  end
@@ -92,9 +97,11 @@ module HTTPX
92
97
  end
93
98
 
94
99
  module RequestMethods
100
+ attr_reader :auth_token_value
101
+
95
102
  def initialize(*)
96
103
  super
97
- @auth_token_value = nil
104
+ @auth_token_value = @auth_header_value = nil
98
105
  end
99
106
 
100
107
  def authorized?
@@ -102,19 +109,20 @@ module HTTPX
102
109
  end
103
110
 
104
111
  def unauthorize!
105
- return unless (auth_value = @auth_token_value)
112
+ return unless (auth_value = @auth_header_value)
106
113
 
107
114
  @headers.get("authorization").delete(auth_value)
108
115
 
109
- @auth_token_value = nil
116
+ @auth_token_value = @auth_header_value = nil
110
117
  end
111
118
 
112
119
  def authorize(auth_value)
120
+ @auth_header_value = auth_value
113
121
  if (auth_type = @options.auth_header_type)
114
- auth_value = "#{auth_type} #{auth_value}"
122
+ @auth_header_value = "#{auth_type} #{@auth_header_value}"
115
123
  end
116
124
 
117
- @headers.add("authorization", auth_value)
125
+ @headers.add("authorization", @auth_header_value)
118
126
 
119
127
  @auth_token_value = auth_value
120
128
  end
@@ -138,8 +146,14 @@ module HTTPX
138
146
  return unless auth_error?(response, request.options) ||
139
147
  (@options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response))
140
148
 
149
+ # regenerate token before retry, but only if it's the first request from batch failing.
150
+ # otherwise, it means that the first request already passed here, so this request should
151
+ # use whatever was generated for it.
152
+ @auth_header_value_mtx.synchronize do
153
+ @auth_header_value = generate_auth_token if request.auth_token_value == @auth_header_value
154
+ end
155
+
141
156
  request.unauthorize!
142
- @auth_header_value = generate_auth_token
143
157
  end
144
158
 
145
159
  def auth_error?(response, options)