httpx 0.16.1 → 0.18.2

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/doc/release_notes/0_17_0.md +49 -0
  4. data/doc/release_notes/0_18_0.md +69 -0
  5. data/doc/release_notes/0_18_1.md +12 -0
  6. data/doc/release_notes/0_18_2.md +10 -0
  7. data/lib/httpx/adapters/datadog.rb +1 -1
  8. data/lib/httpx/adapters/faraday.rb +5 -3
  9. data/lib/httpx/adapters/webmock.rb +9 -3
  10. data/lib/httpx/altsvc.rb +2 -2
  11. data/lib/httpx/chainable.rb +4 -4
  12. data/lib/httpx/connection/http1.rb +23 -14
  13. data/lib/httpx/connection/http2.rb +35 -17
  14. data/lib/httpx/connection.rb +74 -76
  15. data/lib/httpx/domain_name.rb +1 -1
  16. data/lib/httpx/extensions.rb +50 -4
  17. data/lib/httpx/headers.rb +1 -1
  18. data/lib/httpx/io/ssl.rb +5 -1
  19. data/lib/httpx/io/tls.rb +7 -7
  20. data/lib/httpx/loggable.rb +5 -5
  21. data/lib/httpx/options.rb +35 -13
  22. data/lib/httpx/parser/http1.rb +10 -6
  23. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  24. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  25. data/lib/httpx/plugins/compression.rb +5 -3
  26. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  27. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  28. data/lib/httpx/plugins/expect.rb +7 -3
  29. data/lib/httpx/plugins/grpc/message.rb +2 -2
  30. data/lib/httpx/plugins/grpc.rb +3 -3
  31. data/lib/httpx/plugins/h2c.rb +7 -3
  32. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  33. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  34. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  35. data/lib/httpx/plugins/multipart/part.rb +2 -2
  36. data/lib/httpx/plugins/multipart.rb +16 -2
  37. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  38. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  39. data/lib/httpx/plugins/proxy.rb +6 -4
  40. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  41. data/lib/httpx/plugins/response_cache.rb +88 -0
  42. data/lib/httpx/plugins/retries.rb +36 -14
  43. data/lib/httpx/plugins/stream.rb +3 -4
  44. data/lib/httpx/pool.rb +39 -13
  45. data/lib/httpx/registry.rb +1 -1
  46. data/lib/httpx/request.rb +12 -13
  47. data/lib/httpx/resolver/https.rb +5 -7
  48. data/lib/httpx/resolver/native.rb +4 -2
  49. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  50. data/lib/httpx/resolver/system.rb +2 -0
  51. data/lib/httpx/resolver.rb +2 -2
  52. data/lib/httpx/response.rb +60 -44
  53. data/lib/httpx/selector.rb +16 -19
  54. data/lib/httpx/session.rb +22 -15
  55. data/lib/httpx/session2.rb +1 -1
  56. data/lib/httpx/timers.rb +84 -0
  57. data/lib/httpx/transcoder/body.rb +2 -1
  58. data/lib/httpx/transcoder/form.rb +20 -0
  59. data/lib/httpx/transcoder/json.rb +12 -0
  60. data/lib/httpx/transcoder.rb +62 -1
  61. data/lib/httpx/utils.rb +10 -2
  62. data/lib/httpx/version.rb +1 -1
  63. data/lib/httpx.rb +1 -0
  64. data/sig/buffer.rbs +2 -2
  65. data/sig/chainable.rbs +7 -1
  66. data/sig/connection/http1.rbs +15 -4
  67. data/sig/connection/http2.rbs +19 -5
  68. data/sig/connection.rbs +15 -9
  69. data/sig/headers.rbs +19 -18
  70. data/sig/options.rbs +13 -5
  71. data/sig/parser/http1.rbs +3 -3
  72. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  73. data/sig/plugins/aws_sigv4.rbs +12 -3
  74. data/sig/plugins/basic_authentication.rbs +1 -1
  75. data/sig/plugins/multipart.rbs +64 -8
  76. data/sig/plugins/proxy.rbs +6 -6
  77. data/sig/plugins/response_cache.rbs +35 -0
  78. data/sig/plugins/retries.rbs +3 -0
  79. data/sig/pool.rbs +6 -0
  80. data/sig/request.rbs +11 -8
  81. data/sig/resolver/native.rbs +2 -1
  82. data/sig/resolver/resolver_mixin.rbs +1 -1
  83. data/sig/resolver/system.rbs +3 -1
  84. data/sig/response.rbs +11 -4
  85. data/sig/selector.rbs +8 -6
  86. data/sig/session.rbs +8 -14
  87. data/sig/timers.rbs +32 -0
  88. data/sig/transcoder/form.rbs +1 -0
  89. data/sig/transcoder/json.rbs +1 -0
  90. data/sig/transcoder.rbs +5 -4
  91. data/sig/utils.rbs +4 -0
  92. metadata +18 -17
@@ -70,7 +70,7 @@ module HTTPX
70
70
 
71
71
  @inflight = 0
72
72
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
73
- @keep_alive_timer = nil
73
+ @total_timeout = @options.timeout[:total_timeout]
74
74
 
75
75
  self.addresses = @options.addresses if @options.addresses
76
76
  end
@@ -117,7 +117,8 @@ module HTTPX
117
117
  def coalescable?(connection)
118
118
  if @io.protocol == "h2" &&
119
119
  @origin.scheme == "https" &&
120
- connection.origin.scheme == "https"
120
+ connection.origin.scheme == "https" &&
121
+ @io.can_verify_peer?
121
122
  @io.verify_hostname(connection.origin.host)
122
123
  else
123
124
  @origin == connection.origin
@@ -200,11 +201,9 @@ module HTTPX
200
201
  end
201
202
 
202
203
  def close
203
- @parser.close if @parser
204
- return unless @keep_alive_timer
204
+ transition(:active) if @state == :inactive
205
205
 
206
- @keep_alive_timer.cancel
207
- remove_instance_variable(:@keep_alive_timer)
206
+ @parser.close if @parser
208
207
  end
209
208
 
210
209
  def reset
@@ -216,26 +215,40 @@ module HTTPX
216
215
  def send(request)
217
216
  if @parser && !@write_buffer.full?
218
217
  request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
219
- if @keep_alive_timer
218
+
219
+ if @response_received_at && @keep_alive_timeout &&
220
+ Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
220
221
  # when pushing a request into an existing connection, we have to check whether there
221
222
  # is the possibility that the connection might have extended the keep alive timeout.
222
223
  # for such cases, we want to ping for availability before deciding to shovel requests.
223
- if @keep_alive_timer.fires_in.negative?
224
- @pending << request
225
- parser.ping
226
- return
227
- end
228
-
229
- @keep_alive_timer.pause
224
+ @pending << request
225
+ parser.ping
226
+ transition(:active) if @state == :inactive
227
+ return
230
228
  end
231
- @inflight += 1
232
- parser.send(request)
229
+
230
+ send_request_to_parser(request)
233
231
  else
234
232
  @pending << request
235
233
  end
236
234
  end
237
235
 
238
236
  def timeout
237
+ if @total_timeout
238
+ return @total_timeout unless @connected_at
239
+
240
+ elapsed_time = @total_timeout - Utils.elapsed_time(@connected_at)
241
+
242
+ if elapsed_time.negative?
243
+ ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
244
+ ex.set_backtrace(caller)
245
+ on_error(ex)
246
+ return
247
+ end
248
+
249
+ return elapsed_time
250
+ end
251
+
239
252
  return @timeout if defined?(@timeout)
240
253
 
241
254
  return @options.timeout[:connect_timeout] if @state == :idle
@@ -243,6 +256,14 @@ module HTTPX
243
256
  @options.timeout[:operation_timeout]
244
257
  end
245
258
 
259
+ def deactivate
260
+ transition(:inactive)
261
+ end
262
+
263
+ def open?
264
+ @state == :open || @state == :inactive
265
+ end
266
+
246
267
  private
247
268
 
248
269
  def connect
@@ -313,7 +334,7 @@ module HTTPX
313
334
 
314
335
  # exit #consume altogether if all outstanding requests have been dealt with
315
336
  return if @pending.size.zero? && @inflight.zero?
316
- end unless (interests.nil? || interests == :w || @state == :closing) && !epiped
337
+ end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
317
338
 
318
339
  #
319
340
  # tight write loop.
@@ -360,19 +381,18 @@ module HTTPX
360
381
  break if interests == :r || @state == :closing || @state == :closed
361
382
 
362
383
  write_drained = false
363
- end unless interests == :r
384
+ end unless (ints = interests) == :r
364
385
 
365
386
  send_pending if @state == :open
366
387
 
367
388
  # return if socket is drained
368
- next unless (interests != :r || read_drained) &&
369
- (interests != :w || write_drained)
389
+ next unless (ints != :r || read_drained) && (ints != :w || write_drained)
370
390
 
371
391
  # gotta go back to the event loop. It happens when:
372
392
  #
373
393
  # * the socket is drained of bytes or it's not the interest of the conn to read;
374
394
  # * theres nothing more to write, or it's not in the interest of the conn to write;
375
- log(level: 3) { "(#{interests}): WAITING FOR EVENTS..." }
395
+ log(level: 3) { "(#{ints}): WAITING FOR EVENTS..." }
376
396
  return
377
397
  end
378
398
  end
@@ -380,9 +400,7 @@ module HTTPX
380
400
 
381
401
  def send_pending
382
402
  while !@write_buffer.full? && (request = @pending.shift)
383
- @inflight += 1
384
- @keep_alive_timer.pause if @keep_alive_timer
385
- parser.send(request)
403
+ send_request_to_parser(request)
386
404
  end
387
405
  end
388
406
 
@@ -390,6 +408,15 @@ module HTTPX
390
408
  @parser ||= build_parser
391
409
  end
392
410
 
411
+ def send_request_to_parser(request)
412
+ @inflight += 1
413
+ parser.send(request)
414
+
415
+ return unless @state == :inactive
416
+
417
+ transition(:active)
418
+ end
419
+
393
420
  def build_parser(protocol = @io.protocol)
394
421
  parser = registry(protocol).new(@write_buffer, @options)
395
422
  set_parser_callbacks(parser)
@@ -401,7 +428,8 @@ module HTTPX
401
428
  AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
402
429
  emit(:altsvc, alt_origin, origin, alt_params)
403
430
  end
404
- handle_response
431
+ @response_received_at = Utils.now
432
+ @inflight -= 1
405
433
  request.emit(:response, response)
406
434
  end
407
435
  parser.on(:altsvc) do |alt_origin, origin, alt_params|
@@ -421,7 +449,7 @@ module HTTPX
421
449
  end
422
450
  parser.on(:close) do |force|
423
451
  transition(:closing)
424
- if force
452
+ if force || @state == :idle
425
453
  transition(:closed)
426
454
  emit(:close)
427
455
  end
@@ -436,6 +464,7 @@ module HTTPX
436
464
  transition(:closing)
437
465
  transition(:closed)
438
466
  emit(:reset)
467
+
439
468
  @parser.reset if @parser
440
469
  transition(:idle)
441
470
  transition(:open)
@@ -467,15 +496,17 @@ module HTTPX
467
496
  when :open
468
497
  return if @state == :closed
469
498
 
470
- total_timeout
471
-
472
499
  @io.connect
473
500
  return unless @io.connected?
474
501
 
502
+ @connected_at = Utils.now
503
+
475
504
  send_pending
476
505
 
477
506
  @timeout = @current_timeout = parser.timeout
478
507
  emit(:open)
508
+ when :inactive
509
+ return unless @state == :open
479
510
  when :closing
480
511
  return unless @state == :open
481
512
 
@@ -483,15 +514,15 @@ module HTTPX
483
514
  return unless @state == :closing
484
515
  return unless @write_buffer.empty?
485
516
 
486
- if @total_timeout
487
- @total_timeout.cancel
488
- remove_instance_variable(:@total_timeout)
489
- end
490
-
491
517
  purge_after_closed
492
518
  when :already_open
493
519
  nextstate = :open
494
520
  send_pending
521
+ when :active
522
+ return unless @state == :inactive
523
+
524
+ nextstate = :open
525
+ emit(:activate)
495
526
  end
496
527
  @state = nextstate
497
528
  rescue Errno::ECONNREFUSED,
@@ -507,44 +538,24 @@ module HTTPX
507
538
  def purge_after_closed
508
539
  @io.close if @io
509
540
  @read_buffer.clear
510
- if @keep_alive_timer
511
- @keep_alive_timer.cancel
512
- remove_instance_variable(:@keep_alive_timer)
513
- end
514
-
515
541
  remove_instance_variable(:@timeout) if defined?(@timeout)
516
542
  end
517
543
 
518
- def handle_response
519
- @inflight -= 1
520
- return unless @inflight.zero?
521
-
522
- if @keep_alive_timer
523
- @keep_alive_timer.resume
524
- @keep_alive_timer.reset
525
- else
526
- @keep_alive_timer = @timers.after(@keep_alive_timeout) do
527
- unless @inflight.zero?
528
- log { "(#{@origin}): keep alive timeout expired" }
529
- parser.ping
530
- end
531
- end
532
- end
533
- end
534
-
535
544
  def on_error(error)
536
545
  if error.instance_of?(TimeoutError)
537
- if @timeout
538
- @timeout -= error.timeout
539
- return unless @timeout <= 0
540
- end
541
546
 
542
- if @total_timeout && @total_timeout.fires_in.negative?
543
- ex = TotalTimeoutError.new(@total_timeout.interval, "Timed out after #{@total_timeout.interval} seconds")
547
+ if @total_timeout && @connected_at &&
548
+ Utils.elapsed_time(@connected_at) > @total_timeout
549
+ ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
544
550
  ex.set_backtrace(error.backtrace)
545
551
  error = ex
546
- elsif connecting?
547
- error = error.to_connection_error
552
+ else
553
+ if @timeout
554
+ @timeout -= error.timeout
555
+ return unless @timeout <= 0
556
+ end
557
+
558
+ error = error.to_connection_error if connecting?
548
559
  end
549
560
  end
550
561
  handle_error(error)
@@ -559,18 +570,5 @@ module HTTPX
559
570
  request.emit(:response, response)
560
571
  end
561
572
  end
562
-
563
- def total_timeout
564
- total = @options.timeout[:total_timeout]
565
-
566
- return unless total
567
-
568
- @total_timeout ||= @timers.after(total) do
569
- ex = TotalTimeoutError.new(total, "Timed out after #{total} seconds")
570
- ex.set_backtrace(caller)
571
- on_error(ex)
572
- @parser.close if @parser
573
- end
574
- end
575
573
  end
576
574
  end
@@ -123,7 +123,7 @@ module HTTPX
123
123
 
124
124
  # RFC 6265 #4.1.1
125
125
  # Domain-value must be a subdomain.
126
- @domain && self <= domain && domain <= @domain ? true : false
126
+ @domain && self <= domain && domain <= @domain
127
127
  end
128
128
 
129
129
  # def ==(other)
@@ -54,6 +54,51 @@ module HTTPX
54
54
  Numeric.__send__(:include, NegMethods)
55
55
  end
56
56
 
57
+ module HashExtensions
58
+ refine Hash do
59
+ def compact
60
+ h = {}
61
+ each do |key, value|
62
+ h[key] = value unless value == nil
63
+ end
64
+ h
65
+ end unless Hash.method_defined?(:compact)
66
+ end
67
+ end
68
+
69
+ module ArrayExtensions
70
+ refine Array do
71
+
72
+ def filter_map
73
+ return to_enum(:filter_map) unless block_given?
74
+
75
+ each_with_object([]) do |item, res|
76
+ processed = yield(item)
77
+ res << processed if processed
78
+ end
79
+ end unless Array.method_defined?(:filter_map)
80
+
81
+ def sum(accumulator = 0, &block)
82
+ values = block_given? ? map(&block) : self
83
+ values.inject(accumulator, :+)
84
+ end unless Array.method_defined?(:sum)
85
+ end
86
+ end
87
+
88
+ module IOExtensions
89
+ refine IO do
90
+ # provides a fallback for rubies where IO#wait isn't implemented,
91
+ # but IO#wait_readable and IO#wait_writable are.
92
+ def wait(timeout = nil, _mode = :read_write)
93
+ r, w = IO.select([self], [self], nil, timeout)
94
+
95
+ return unless r || w
96
+
97
+ self
98
+ end unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
99
+ end
100
+ end
101
+
57
102
  module RegexpExtensions
58
103
  # If you wonder why this is there: the oauth feature uses a refinement to enhance the
59
104
  # Regexp class locally with #match? , but this is never tested, because ActiveSupport
@@ -77,13 +122,14 @@ module HTTPX
77
122
  end
78
123
 
79
124
  def authority
80
- port_string = port == default_port ? nil : ":#{port}"
81
- "#{host}#{port_string}"
82
- end
125
+ return host if port == default_port
126
+
127
+ "#{host}:#{port}"
128
+ end unless URI::HTTP.method_defined?(:authority)
83
129
 
84
130
  def origin
85
131
  "#{scheme}://#{authority}"
86
- end
132
+ end unless URI::HTTP.method_defined?(:origin)
87
133
 
88
134
  def altsvc_match?(uri)
89
135
  uri = URI.parse(uri)
data/lib/httpx/headers.rb CHANGED
@@ -57,7 +57,7 @@ module HTTPX
57
57
  def merge(other)
58
58
  headers = dup
59
59
  other.each do |field, value|
60
- headers[field] = value
60
+ headers[downcased(field)] = value
61
61
  end
62
62
  headers
63
63
  end
data/lib/httpx/io/ssl.rb CHANGED
@@ -27,6 +27,10 @@ module HTTPX
27
27
  super
28
28
  end
29
29
 
30
+ def can_verify_peer?
31
+ @ctx.verify_mode == OpenSSL::SSL::VERIFY_PEER
32
+ end
33
+
30
34
  def verify_hostname(host)
31
35
  return false if @ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
32
36
  return false if !@io.respond_to?(:peer_cert) || @io.peer_cert.nil?
@@ -134,7 +138,7 @@ module HTTPX
134
138
  server_cert = @io.peer_cert
135
139
 
136
140
  "#{super}\n\n" \
137
- "SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
141
+ "SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
138
142
  "ALPN, server accepted to use #{protocol}\n" \
139
143
  "Server certificate:\n" \
140
144
  " subject: #{server_cert.subject}\n" \
data/lib/httpx/io/tls.rb CHANGED
@@ -194,15 +194,15 @@ module HTTPX
194
194
  server_cert = @peer_cert
195
195
 
196
196
  "#{super}\n\n" \
197
- "SSL connection using #{@ctx.ssl_version} / #{Array(@ctx.cipher).first}\n" \
198
- "ALPN, server accepted to use #{protocol}\n" +
197
+ "SSL connection using #{@ctx.ssl_version} / #{Array(@ctx.cipher).first}\n" \
198
+ "ALPN, server accepted to use #{protocol}\n" +
199
199
  (if server_cert
200
200
  "Server certificate:\n" \
201
- " subject: #{server_cert.subject}\n" \
202
- " start date: #{server_cert.not_before}\n" \
203
- " expire date: #{server_cert.not_after}\n" \
204
- " issuer: #{server_cert.issuer}\n" \
205
- " SSL certificate verify ok."
201
+ " subject: #{server_cert.subject}\n" \
202
+ " start date: #{server_cert.not_before}\n" \
203
+ " expire date: #{server_cert.not_after}\n" \
204
+ " issuer: #{server_cert.issuer}\n" \
205
+ " SSL certificate verify ok."
206
206
  else
207
207
  "SSL certificate verify failed."
208
208
  end
@@ -24,15 +24,13 @@ module HTTPX
24
24
  debug_stream << message
25
25
  end
26
26
 
27
- if !Exception.instance_methods.include?(:full_message)
27
+ if Exception.instance_methods.include?(:full_message)
28
28
 
29
29
  def log_exception(ex, level: @options.debug_level, color: nil)
30
30
  return unless @options.debug
31
31
  return unless @options.debug_level >= level
32
32
 
33
- message = +"#{ex.message} (#{ex.class})"
34
- message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
35
- log(level: level, color: color) { message }
33
+ log(level: level, color: color) { ex.full_message }
36
34
  end
37
35
 
38
36
  else
@@ -41,7 +39,9 @@ module HTTPX
41
39
  return unless @options.debug
42
40
  return unless @options.debug_level >= level
43
41
 
44
- log(level: level, color: color) { ex.full_message }
42
+ message = +"#{ex.message} (#{ex.class})"
43
+ message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
44
+ log(level: level, color: color) { message }
45
45
  end
46
46
 
47
47
  end
data/lib/httpx/options.rb CHANGED
@@ -39,6 +39,28 @@ module HTTPX
39
39
  :resolver_options => { cache: true },
40
40
  }.freeze
41
41
 
42
+ begin
43
+ module HashExtensions
44
+ refine Hash do
45
+ def >=(other)
46
+ Hash[other] <= self
47
+ end
48
+
49
+ def <=(other)
50
+ other = Hash[other]
51
+ return false unless size <= other.size
52
+
53
+ each do |k, v|
54
+ v2 = other.fetch(k) { return false }
55
+ return false unless v2 == v
56
+ end
57
+ true
58
+ end
59
+ end
60
+ end
61
+ using HashExtensions
62
+ end unless Hash.method_defined?(:>=)
63
+
42
64
  class << self
43
65
  def new(options = {})
44
66
  # let enhanced options go through
@@ -59,9 +81,9 @@ module HTTPX
59
81
  end
60
82
 
61
83
  def def_option(optname, *args, &block)
62
- if args.size.zero? && !block_given?
84
+ if args.size.zero? && !block
63
85
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
64
- def option_#{optname}(v); v; end
86
+ def option_#{optname}(v); v; end # def option_smth(v); v; end
65
87
  OUT
66
88
  return
67
89
  end
@@ -71,15 +93,15 @@ module HTTPX
71
93
 
72
94
  def deprecated_def_option(optname, layout = nil, &interpreter)
73
95
  warn "DEPRECATION WARNING: using `def_option(#{optname})` for setting options is deprecated. " \
74
- "Define module OptionsMethods and `def option_#{optname}(val)` instead."
96
+ "Define module OptionsMethods and `def option_#{optname}(val)` instead."
75
97
 
76
98
  if layout
77
99
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
78
- def option_#{optname}(value)
79
- #{layout}
80
- end
100
+ def option_#{optname}(value) # def option_origin(v)
101
+ #{layout} # URI(v)
102
+ end # end
81
103
  OUT
82
- elsif block_given?
104
+ elsif interpreter
83
105
  define_method(:"option_#{optname}") do |value|
84
106
  instance_exec(value, &interpreter)
85
107
  end
@@ -89,7 +111,7 @@ module HTTPX
89
111
 
90
112
  def initialize(options = {})
91
113
  defaults = DEFAULT_OPTIONS.merge(options)
92
- defaults.each do |(k, v)|
114
+ defaults.each do |k, v|
93
115
  next if v.nil?
94
116
 
95
117
  begin
@@ -163,6 +185,7 @@ module HTTPX
163
185
  end
164
186
 
165
187
  REQUEST_IVARS = %i[@params @form @json @body].freeze
188
+ private_constant :REQUEST_IVARS
166
189
 
167
190
  def ==(other)
168
191
  ivars = instance_variables | other.instance_variables
@@ -180,14 +203,14 @@ module HTTPX
180
203
  end
181
204
 
182
205
  def merge(other)
183
- raise ArgumentError, "#{other.inspect} is not a valid set of options" unless other.respond_to?(:to_hash)
206
+ raise ArgumentError, "#{other} is not a valid set of options" unless other.respond_to?(:to_hash)
184
207
 
185
208
  h2 = other.to_hash
186
209
  return self if h2.empty?
187
210
 
188
211
  h1 = to_hash
189
212
 
190
- return self if h1 == h2
213
+ return self if h1 >= h2
191
214
 
192
215
  merged = h1.merge(h2) do |_k, v1, v2|
193
216
  if v1.respond_to?(:merge) && v2.respond_to?(:merge)
@@ -201,10 +224,9 @@ module HTTPX
201
224
  end
202
225
 
203
226
  def to_hash
204
- hash_pairs = instance_variables.map do |ivar|
205
- [ivar[1..-1].to_sym, instance_variable_get(ivar)]
227
+ instance_variables.each_with_object({}) do |ivar, hs|
228
+ hs[ivar[1..-1].to_sym] = instance_variable_get(ivar)
206
229
  end
207
- Hash[hash_pairs]
208
230
  end
209
231
 
210
232
  if RUBY_VERSION > "2.4.0"
@@ -60,7 +60,7 @@ module HTTPX
60
60
  (m = %r{\AHTTP(?:/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
61
61
  raise(Error, "wrong head line format")
62
62
  version, code, _ = m.captures
63
- raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version)
63
+ raise(Error, "unsupported HTTP version (HTTP/#{version})") unless version && VERSIONS.include?(version)
64
64
 
65
65
  @http_version = version.split(".").map(&:to_i)
66
66
  @status_code = code.to_i
@@ -72,9 +72,14 @@ module HTTPX
72
72
 
73
73
  def parse_headers
74
74
  headers = @headers
75
- while (idx = @buffer.index("\n"))
76
- line = @buffer.byteslice(0..idx).sub(/\s+\z/, "")
77
- @buffer = @buffer.byteslice((idx + 1)..-1)
75
+ buffer = @buffer
76
+
77
+ while (idx = buffer.index("\n"))
78
+ line = buffer.byteslice(0..idx)
79
+ raise Error, "wrong header format" if line.start_with?("\s", "\t")
80
+
81
+ line.lstrip!
82
+ buffer = @buffer = buffer.byteslice((idx + 1)..-1)
78
83
  if line.empty?
79
84
  case @state
80
85
  when :headers
@@ -97,9 +102,8 @@ module HTTPX
97
102
  raise Error, "wrong header format" unless separator_index
98
103
 
99
104
  key = line.byteslice(0..(separator_index - 1))
100
- raise Error, "wrong header format" if key.start_with?("\s", "\t")
101
105
 
102
- key.strip!
106
+ key.rstrip! # was lstripped previously!
103
107
  value = line.byteslice((separator_index + 1)..-1)
104
108
  value.strip!
105
109
  raise Error, "wrong header format" if value.nil?
@@ -8,6 +8,23 @@ module HTTPX
8
8
  # It requires the "aws-sdk-core" gem.
9
9
  #
10
10
  module AwsSdkAuthentication
11
+ # Mock configuration, to be used only when resolving credentials
12
+ class Configuration
13
+ attr_reader :profile
14
+
15
+ def initialize(profile)
16
+ @profile = profile
17
+ end
18
+
19
+ def respond_to_missing?(*)
20
+ true
21
+ end
22
+
23
+ def method_missing(*)
24
+ nil
25
+ end
26
+ end
27
+
11
28
  #
12
29
  # encapsulates access to an AWS SDK credentials store.
13
30
  #
@@ -30,23 +47,8 @@ module HTTPX
30
47
  end
31
48
 
32
49
  class << self
33
- attr_reader :credentials, :region
34
-
35
50
  def load_dependencies(_klass)
36
51
  require "aws-sdk-core"
37
-
38
- client = Class.new(Seahorse::Client::Base) do
39
- @identifier = :httpx
40
- set_api(Aws::S3::ClientApi::API)
41
- add_plugin(Aws::Plugins::CredentialsConfiguration)
42
- add_plugin(Aws::Plugins::RegionalEndpoint)
43
- class << self
44
- attr_reader :identifier
45
- end
46
- end.new
47
-
48
- @credentials = Credentials.new(client.config[:credentials])
49
- @region = client.config[:region]
50
52
  end
51
53
 
52
54
  def configure(klass)
@@ -56,6 +58,26 @@ module HTTPX
56
58
  def extra_options(options)
57
59
  options.merge(max_concurrent_requests: 1)
58
60
  end
61
+
62
+ def credentials(profile)
63
+ mock_configuration = Configuration.new(profile)
64
+ Credentials.new(Aws::CredentialProviderChain.new(mock_configuration).resolve)
65
+ end
66
+
67
+ def region(profile)
68
+ # https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-core/lib/aws-sdk-core/plugins/regional_endpoint.rb#L62
69
+ keys = %w[AWS_REGION AMAZON_REGION AWS_DEFAULT_REGION]
70
+ env_region = ENV.values_at(*keys).compact.first
71
+ env_region = nil if env_region == ""
72
+ cfg_region = Aws.shared_config.region(profile: profile)
73
+ env_region || cfg_region
74
+ end
75
+ end
76
+
77
+ module OptionsMethods
78
+ def option_aws_profile(value)
79
+ String(value)
80
+ end
59
81
  end
60
82
 
61
83
  module InstanceMethods
@@ -64,9 +86,11 @@ module HTTPX
64
86
  # aws_authentication(credentials: Aws::Credentials.new('akid', 'secret'))
65
87
  # aws_authentication()
66
88
  #
67
- def aws_sdk_authentication(**options)
68
- credentials = AwsSdkAuthentication.credentials
69
- region = AwsSdkAuthentication.region
89
+ def aws_sdk_authentication(
90
+ credentials: AwsSdkAuthentication.credentials(@options.aws_profile),
91
+ region: AwsSdkAuthentication.region(@options.aws_profile),
92
+ **options
93
+ )
70
94
 
71
95
  aws_sigv4_authentication(
72
96
  credentials: credentials,