httpx 0.16.1 → 0.18.2

Sign up to get free protection for your applications and to get access to all the features.
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,