httpx 1.8.4 → 1.9.1

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_8_1.md +1 -1
  3. data/doc/release_notes/1_8_2.md +4 -4
  4. data/doc/release_notes/1_8_3.md +3 -5
  5. data/doc/release_notes/1_8_4.md +1 -1
  6. data/doc/release_notes/1_9_0.md +75 -0
  7. data/doc/release_notes/1_9_1.md +32 -0
  8. data/lib/httpx/adapters/faraday.rb +1 -1
  9. data/lib/httpx/adapters/webmock.rb +15 -2
  10. data/lib/httpx/altsvc.rb +29 -15
  11. data/lib/httpx/chainable.rb +4 -1
  12. data/lib/httpx/connection/http1.rb +34 -35
  13. data/lib/httpx/connection/http2.rb +146 -33
  14. data/lib/httpx/connection.rb +282 -162
  15. data/lib/httpx/headers.rb +2 -2
  16. data/lib/httpx/io/tcp.rb +42 -21
  17. data/lib/httpx/options.rb +86 -35
  18. data/lib/httpx/parser/http1.rb +3 -6
  19. data/lib/httpx/plugins/auth/digest.rb +1 -2
  20. data/lib/httpx/plugins/auth.rb +11 -2
  21. data/lib/httpx/plugins/aws_sdk_authentication.rb +7 -3
  22. data/lib/httpx/plugins/aws_sigv4.rb +1 -1
  23. data/lib/httpx/plugins/basic_auth.rb +2 -0
  24. data/lib/httpx/plugins/cache.rb +1 -1
  25. data/lib/httpx/plugins/callbacks.rb +8 -6
  26. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  27. data/lib/httpx/plugins/content_digest.rb +3 -5
  28. data/lib/httpx/plugins/digest_auth.rb +2 -0
  29. data/lib/httpx/plugins/expect.rb +10 -1
  30. data/lib/httpx/plugins/fiber_concurrency.rb +59 -16
  31. data/lib/httpx/plugins/follow_redirects.rb +1 -1
  32. data/lib/httpx/plugins/grpc.rb +21 -0
  33. data/lib/httpx/plugins/h2c.rb +12 -11
  34. data/lib/httpx/plugins/hedging.rb +284 -0
  35. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  36. data/lib/httpx/plugins/ntlm_v2_auth.rb +5 -3
  37. data/lib/httpx/plugins/oauth.rb +2 -2
  38. data/lib/httpx/plugins/proxy/http.rb +67 -26
  39. data/lib/httpx/plugins/proxy/socks5.rb +4 -4
  40. data/lib/httpx/plugins/proxy/ssh.rb +30 -11
  41. data/lib/httpx/plugins/proxy.rb +68 -54
  42. data/lib/httpx/plugins/push_promise.rb +4 -2
  43. data/lib/httpx/plugins/query.rb +1 -0
  44. data/lib/httpx/plugins/retries.rb +2 -2
  45. data/lib/httpx/plugins/server_sent_events.rb +1 -0
  46. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  47. data/lib/httpx/plugins/stream.rb +12 -10
  48. data/lib/httpx/plugins/stream_bidi.rb +4 -0
  49. data/lib/httpx/plugins/tracing.rb +99 -11
  50. data/lib/httpx/plugins/upgrade/h2.rb +3 -3
  51. data/lib/httpx/plugins/upgrade.rb +2 -0
  52. data/lib/httpx/plugins/webdav.rb +8 -0
  53. data/lib/httpx/request/body.rb +2 -1
  54. data/lib/httpx/request.rb +6 -6
  55. data/lib/httpx/resolver/https.rb +3 -2
  56. data/lib/httpx/resolver/native.rb +19 -9
  57. data/lib/httpx/resolver/resolver.rb +2 -2
  58. data/lib/httpx/resolver/system.rb +4 -0
  59. data/lib/httpx/resolver.rb +10 -6
  60. data/lib/httpx/response.rb +19 -7
  61. data/lib/httpx/selector.rb +111 -10
  62. data/lib/httpx/session.rb +36 -10
  63. data/lib/httpx/transcoder/json.rb +1 -1
  64. data/lib/httpx/version.rb +1 -1
  65. data/lib/httpx.rb +1 -0
  66. data/sig/altsvc.rbs +3 -3
  67. data/sig/callbacks.rbs +1 -0
  68. data/sig/chainable.rbs +3 -1
  69. data/sig/connection/http1.rbs +4 -2
  70. data/sig/connection/http2.rbs +23 -17
  71. data/sig/connection.rbs +29 -7
  72. data/sig/headers.rbs +1 -1
  73. data/sig/httpx.rbs +2 -0
  74. data/sig/io/tcp.rbs +5 -0
  75. data/sig/options.rbs +43 -12
  76. data/sig/parser/http1.rbs +1 -1
  77. data/sig/plugins/auth/digest.rbs +0 -1
  78. data/sig/plugins/auth.rbs +12 -5
  79. data/sig/plugins/expect.rbs +6 -0
  80. data/sig/plugins/fiber_concurrency.rbs +10 -1
  81. data/sig/plugins/hedging.rbs +44 -0
  82. data/sig/plugins/proxy/http.rbs +9 -2
  83. data/sig/plugins/proxy.rbs +3 -2
  84. data/sig/plugins/retries.rbs +2 -2
  85. data/sig/plugins/stream_bidi.rbs +0 -2
  86. data/sig/plugins/tracing.rbs +22 -5
  87. data/sig/request/body.rbs +1 -1
  88. data/sig/resolver/entry.rbs +2 -2
  89. data/sig/resolver/https.rbs +2 -0
  90. data/sig/resolver/native.rbs +3 -0
  91. data/sig/resolver/resolver.rbs +1 -1
  92. data/sig/resolver/system.rbs +2 -0
  93. data/sig/resolver.rbs +1 -1
  94. data/sig/response.rbs +1 -2
  95. data/sig/selector.rbs +17 -3
  96. data/sig/session.rbs +8 -6
  97. metadata +7 -1
@@ -63,13 +63,21 @@ module HTTPX::Plugins
63
63
  end
64
64
 
65
65
  module RequestMethods
66
+ # time at which a request started being buffered for sending.
66
67
  attr_accessor :init_time
67
68
 
69
+ # when not nil, it means this request was sent to a non-open connection, and contains how much time it took
70
+ # the connection to connect.
71
+ attr_accessor :handshake_time
72
+
73
+ # time at which the request was first handed to a connection, or last moved back to waiting for one.
74
+ attr_accessor :enqueue_time
75
+
68
76
  # intercepts request initialization to inject the tracing logic.
69
77
  def initialize(*)
70
78
  super
71
79
 
72
- @init_time = nil
80
+ @init_time = @handshake_time = @enqueue_time = nil
73
81
 
74
82
  tracer = @options.tracer
75
83
 
@@ -79,12 +87,15 @@ module HTTPX::Plugins
79
87
  tracer.reset(self)
80
88
 
81
89
  # request is reset when it's retried.
82
- @init_time = nil
90
+ @init_time = @handshake_time = nil
91
+
92
+ # going back to idle means waiting to be sent again.
93
+ @enqueue_time = ::Time.now.utc
83
94
  end
84
95
  on(:headers) do
85
96
  # the usual request init time (when not including the connection handshake)
86
97
  # should be the time the request is buffered the first time.
87
- @init_time ||= ::Time.now.utc
98
+ @init_time = calculate_init_time
88
99
 
89
100
  tracer.start(self)
90
101
  end
@@ -92,15 +103,31 @@ module HTTPX::Plugins
92
103
  end
93
104
 
94
105
  def response=(*)
95
- # init_time should be set when it's send to a connection.
96
- # However, there are situations where connection initialization fails.
106
+ # There are situations where connection initialization fails.
97
107
  # Example is the :ssrf_filter plugin, which raises an error on
98
108
  # initialize if the host is an IP which matches against the known set.
99
109
  # in such cases, we'll just set here right here.
100
- @init_time ||= ::Time.now.utc
110
+ @init_time ||= calculate_init_time
101
111
 
102
112
  super
103
113
  end
114
+
115
+ private
116
+
117
+ def calculate_init_time
118
+ init_time = ::Time.now.utc
119
+
120
+ return init_time unless (handshake_time = @handshake_time)
121
+
122
+ init_time -= handshake_time
123
+
124
+ enqueue_time = @enqueue_time
125
+
126
+ # handshake time is measured for the connection, and may happen before the request was sent to it.
127
+ return init_time unless enqueue_time && init_time < enqueue_time
128
+
129
+ enqueue_time
130
+ end
104
131
  end
105
132
 
106
133
  # Connection mixin
@@ -111,12 +138,20 @@ module HTTPX::Plugins
111
138
  @init_time = nil
112
139
  end
113
140
 
114
- def send_request_to_parser(request)
115
- if connecting?
116
- @init_time ||= ::Time.now.utc
141
+ def send(request)
142
+ # requests may be rerouted to other connections (i.e. when coalescing), so only the first
143
+ # connection the request is handed to marks it.
144
+ request.enqueue_time ||= ::Time.now.utc
145
+
146
+ super
147
+ end
117
148
 
118
- # request span timeframe should include the time it took to connect.
119
- request.init_time ||= @init_time
149
+ def send_request_to_parser(request, *)
150
+ if connecting? && @init_time
151
+ # at this point, the connection has connected, but hasn't yet transitioned to :open,
152
+ # so it's the time the requests which caused this connection to connect, are finally being
153
+ # sent to the parser, so handshake time should factor into the span time.
154
+ request.handshake_time ||= ::Time.now.utc - @init_time
120
155
  end
121
156
 
122
157
  super
@@ -135,8 +170,37 @@ module HTTPX::Plugins
135
170
  @init_time = nil
136
171
  end
137
172
 
173
+ def disconnect
174
+ return if @exhausted
175
+
176
+ return unless @current_session && @current_selector
177
+
178
+ # reset init time of connections abandoned while connecting.
179
+ @init_time = nil if connecting?
180
+
181
+ super
182
+ end
183
+
138
184
  private
139
185
 
186
+ def handle_transition(*)
187
+ super
188
+
189
+ # no longer relevant after connection established.
190
+ @init_time = nil unless connecting?
191
+ end
192
+
193
+ def handle_error(er, request = nil)
194
+ if connecting? && @init_time
195
+ # bookkeep handshake time back into pending requests, which were never initialized with it.
196
+ handshake_time = ::Time.now.utc - @init_time
197
+ request.handshake_time ||= handshake_time if request
198
+ @pending.each { |req| req.handshake_time ||= handshake_time }
199
+ end
200
+
201
+ super
202
+ end
203
+
140
204
  def connect
141
205
  @init_time ||= ::Time.now.utc
142
206
 
@@ -151,6 +215,30 @@ module HTTPX::Plugins
151
215
  super
152
216
  end
153
217
  end
218
+
219
+ module HTTP2Methods
220
+ def initialize(*)
221
+ super
222
+
223
+ @handshake_init_time = ::Time.now.utc
224
+ end
225
+
226
+ private
227
+
228
+ def on_settings(*)
229
+ unless @handshake_completed
230
+ # pending requests must wait for the handshake completion
231
+ handshake_time = ::Time.now.utc - @handshake_init_time
232
+
233
+ @pending.each do |request|
234
+ request.handshake_time ||= 0.0
235
+ request.handshake_time += handshake_time
236
+ end
237
+ end
238
+
239
+ super
240
+ end
241
+ end
154
242
  end
155
243
  register_plugin :tracing, Tracing
156
244
  end
@@ -25,7 +25,7 @@ module HTTPX
25
25
  def interests
26
26
  return super unless connecting? && @parser
27
27
 
28
- connect
28
+ try_connect
29
29
 
30
30
  return @io.interests if connecting?
31
31
 
@@ -33,9 +33,9 @@ module HTTPX
33
33
  end
34
34
 
35
35
  def upgrade_to_h2
36
- enqueue_pending_requests_from_parser(@parser)
36
+ @parser.reset_requests
37
37
 
38
- @parser = @options.http2_class.new(@write_buffer, @options)
38
+ @parser = @options.http2_class.new(@write_buffer, @pending, @options)
39
39
  set_parser_callbacks(@parser)
40
40
  @upgrade_protocol = "h2"
41
41
 
@@ -30,6 +30,8 @@ module HTTPX
30
30
  end
31
31
 
32
32
  module InstanceMethods
33
+ private
34
+
33
35
  def fetch_response(request, selector, options)
34
36
  response = super
35
37
 
@@ -13,14 +13,18 @@ module HTTPX
13
13
  end
14
14
 
15
15
  module InstanceMethods
16
+ # sends a COPY WebDav request for +src+ and +dest+.
16
17
  def copy(src, dest)
17
18
  request("COPY", src, headers: { "destination" => @options.origin.merge(dest) })
18
19
  end
19
20
 
21
+ # sends a MOVE WebDav request for +src+ and +dest+.
20
22
  def move(src, dest)
21
23
  request("MOVE", src, headers: { "destination" => @options.origin.merge(dest) })
22
24
  end
23
25
 
26
+ # sends a LOCK WebDav request for +path+ (for the time set by +timeout+, when passed).
27
+ # it calls #unlock after the block is executed.
24
28
  def lock(path, timeout: nil, &blk)
25
29
  headers = {}
26
30
  headers["timeout"] = if timeout && timeout.positive?
@@ -51,14 +55,17 @@ module HTTPX
51
55
  response
52
56
  end
53
57
 
58
+ # sends a UNLOCK WebDav request for +path+, using the given +lock_token+.
54
59
  def unlock(path, lock_token)
55
60
  request("UNLOCK", path, headers: { "lock-token" => lock_token })
56
61
  end
57
62
 
63
+ # sends a MKCOL WebDav request for +dir+.
58
64
  def mkcol(dir)
59
65
  request("MKCOL", dir)
60
66
  end
61
67
 
68
+ # sends a PROPFIND WebDav request for +path+.
62
69
  def propfind(path, xml = nil)
63
70
  body = case xml
64
71
  when :acl
@@ -73,6 +80,7 @@ module HTTPX
73
80
  request("PROPFIND", path, headers: { "depth" => "1" }, xml: body)
74
81
  end
75
82
 
83
+ # sends a PROPPATCH WebDav request for +path+.
76
84
  def proppatch(path, xml)
77
85
  body = "<?xml version=\"1.0\"?>" \
78
86
  "<D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"http://ns.example.com/standards/z39.50/\">#{xml}</D:propertyupdate>"
@@ -32,6 +32,7 @@ module HTTPX
32
32
  @headers = h
33
33
  @body = self.class.initialize_body(params)
34
34
  @options = options.merge(params)
35
+ @unbounded_body = nil
35
36
 
36
37
  if @body
37
38
  if @options.compress_request_body && @headers.key?("content-encoding")
@@ -102,7 +103,7 @@ module HTTPX
102
103
 
103
104
  # returns whether the body yields infinitely.
104
105
  def unbounded_body?
105
- return @unbounded_body if defined?(@unbounded_body)
106
+ return @unbounded_body unless @unbounded_body.nil?
106
107
 
107
108
  @unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
108
109
  end
data/lib/httpx/request.rb CHANGED
@@ -140,22 +140,22 @@ module HTTPX
140
140
 
141
141
  # the read timeout defined for this request.
142
142
  def read_timeout
143
- @options.timeout[:read_timeout]
143
+ @options.timeout.read_timeout
144
144
  end
145
145
 
146
146
  # the write timeout defined for this request.
147
147
  def write_timeout
148
- @options.timeout[:write_timeout]
148
+ @options.timeout.write_timeout
149
149
  end
150
150
 
151
151
  # the request timeout defined for this request.
152
152
  def request_timeout
153
- @options.timeout[:request_timeout]
153
+ @options.timeout.request_timeout
154
154
  end
155
155
 
156
156
  # the total request timeout defined for this request.
157
157
  def total_request_timeout
158
- @options.timeout[:total_request_timeout]
158
+ @options.timeout.total_request_timeout
159
159
  end
160
160
 
161
161
  def persistent?
@@ -180,7 +180,7 @@ module HTTPX
180
180
  end
181
181
 
182
182
  def can_buffer?
183
- @state != :done
183
+ @state != :done && @state != :expect
184
184
  end
185
185
 
186
186
  def started?
@@ -339,7 +339,7 @@ module HTTPX
339
339
 
340
340
  # whether the request supports the 100-continue handshake and already processed the 100 response.
341
341
  def expects?
342
- @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
342
+ @headers.key?("expect") && @headers.get("expect").include?("100-continue") && @informational_status == 100 && !@response
343
343
  end
344
344
 
345
345
  def set_timeout_callback(event, &callback)
@@ -32,7 +32,8 @@ module HTTPX
32
32
  }.freeze
33
33
 
34
34
  def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close,
35
- :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
35
+ :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout,
36
+ :closed_for_select?
36
37
 
37
38
  def initialize(_, options)
38
39
  super
@@ -49,7 +50,7 @@ module HTTPX
49
50
  end
50
51
 
51
52
  def state
52
- @resolver_connection ? @resolver_connection.state : :idle
53
+ @resolver_connection&.state || :idle
53
54
  end
54
55
 
55
56
  def <<(connection)
@@ -38,12 +38,11 @@ module HTTPX
38
38
  @search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
39
39
  @_timeouts = Array(@resolver_options[:timeouts])
40
40
  @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
41
- @name = nil
41
+ @name = @message_id = @timer = nil
42
42
  @queries = {}
43
43
  @read_buffer = "".b
44
44
  @write_buffer = Buffer.new(@resolver_options[:packet_size])
45
45
  @state = :idle
46
- @timer = nil
47
46
  end
48
47
 
49
48
  def close
@@ -52,7 +51,7 @@ module HTTPX
52
51
 
53
52
  def force_close(*)
54
53
  @timer.cancel if @timer
55
- @timer = @name = nil
54
+ @timer = @name = @message_id = nil
56
55
  @queries.clear
57
56
  @timeouts.clear
58
57
  close
@@ -87,6 +86,10 @@ module HTTPX
87
86
  end
88
87
  end
89
88
 
89
+ def closed_for_select?
90
+ @state == :closed
91
+ end
92
+
90
93
  def interests
91
94
  case @state
92
95
  when :idle
@@ -170,6 +173,7 @@ module HTTPX
170
173
  if nameserver && @ns_index < nameserver.size
171
174
  log { "resolver #{FAMILY_TYPES[@record_type]}: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
172
175
  transition(:idle)
176
+ transition(:open)
173
177
  @timeouts.clear
174
178
  retry
175
179
  else
@@ -195,7 +199,7 @@ module HTTPX
195
199
  @timer = @current_selector.after(timeout) do
196
200
  next unless @connections.include?(connection)
197
201
 
198
- @timer = @name = nil
202
+ @timer = @name = @message_id = nil
199
203
 
200
204
  do_retry(h, connection, timeout)
201
205
  end
@@ -310,7 +314,13 @@ module HTTPX
310
314
  end
311
315
 
312
316
  def parse(buffer)
313
- code, result = Resolver.decode_dns_answer(buffer)
317
+ code, result, message_id = Resolver.decode_dns_answer(buffer)
318
+
319
+ # discard late message answers for no longer inflight queries.
320
+ if message_id && message_id != @message_id
321
+ log { "resolver #{FAMILY_TYPES[@record_type]}: discarding answer for a query no longer in flight" }
322
+ return
323
+ end
314
324
 
315
325
  case code
316
326
  when :ok
@@ -392,7 +402,7 @@ module HTTPX
392
402
  orig_name = name
393
403
  # absolute name
394
404
  name_labels = Resolv::DNS::Name.create(name).to_a
395
- name = @queries.each_key.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
405
+ name = @queries.each_key.find { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
396
406
 
397
407
  # probably a retried query for which there's an answer
398
408
  unless name
@@ -462,11 +472,10 @@ module HTTPX
462
472
  @queries[hostname] = connection
463
473
  end
464
474
 
465
- @name = hostname
466
-
467
475
  log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
468
476
  begin
469
477
  @write_buffer << encode_dns_query(hostname)
478
+ @name = hostname
470
479
  rescue Resolv::DNS::EncodeError => e
471
480
  reset_hostname(hostname, connection: connection)
472
481
  @connections.delete(connection)
@@ -479,6 +488,7 @@ module HTTPX
479
488
  message_id = Resolver.generate_id
480
489
  msg = Resolver.encode_dns_query(hostname, type: @record_type, message_id: message_id)
481
490
  msg[0, 2] = [msg.size, message_id].pack("nn") if @socket_type == :tcp
491
+ @message_id = message_id
482
492
  msg
483
493
  end
484
494
 
@@ -574,7 +584,7 @@ module HTTPX
574
584
  def reset_query
575
585
  @timer.cancel
576
586
 
577
- @timer = @name = nil
587
+ @timer = @name = @message_id = nil
578
588
  end
579
589
 
580
590
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
@@ -29,9 +29,9 @@ module HTTPX
29
29
 
30
30
  attr_reader :family, :options
31
31
 
32
- attr_writer :current_selector, :current_session
32
+ attr_writer :current_session
33
33
 
34
- attr_accessor :multi
34
+ attr_accessor :current_selector, :multi
35
35
 
36
36
  def initialize(family, options)
37
37
  @family = family
@@ -90,6 +90,10 @@ module HTTPX
90
90
  nil
91
91
  end
92
92
 
93
+ def closed_for_select?
94
+ @state == :closed
95
+ end
96
+
93
97
  def interests
94
98
  return if @queries.empty?
95
99
 
@@ -51,16 +51,20 @@ module HTTPX
51
51
  end
52
52
 
53
53
  # no domain was found
54
- return :no_domain_found if message.rcode == Resolv::DNS::RCode::NXDomain
54
+ return :no_domain_found, nil, message.id if message.rcode == Resolv::DNS::RCode::NXDomain
55
55
 
56
- return :message_truncated if message.tc == 1
56
+ return :message_truncated, nil, message.id if message.tc == 1
57
57
 
58
58
  if message.rcode != Resolv::DNS::RCode::NoError
59
59
  case message.rcode
60
- when Resolv::DNS::RCode::ServFail
61
- return :retriable_error, message.rcode
60
+ when Resolv::DNS::RCode::ServFail,
61
+ # getaddrinfo also retries these errors, as they're not about the hostname being resolved,
62
+ # but the DNS resolver state.
63
+ Resolv::DNS::RCode::NotImp,
64
+ Resolv::DNS::RCode::Refused
65
+ return :retriable_error, message.rcode, message.id
62
66
  else
63
- return :dns_error, message.rcode
67
+ return :dns_error, message.rcode, message.id
64
68
  end
65
69
  end
66
70
 
@@ -85,7 +89,7 @@ module HTTPX
85
89
  end
86
90
  end
87
91
 
88
- [:ok, addresses]
92
+ [:ok, addresses, message.id]
89
93
  end
90
94
 
91
95
  private
@@ -119,7 +119,6 @@ module HTTPX
119
119
  # marks the response as finished, freezes the headers.
120
120
  def finish!
121
121
  @finished = true
122
- @headers.freeze
123
122
  @request.connection = nil
124
123
  end
125
124
 
@@ -215,8 +214,7 @@ module HTTPX
215
214
 
216
215
  # Helper class which decodes the HTTP "content-type" header.
217
216
  class ContentType
218
- MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
219
- CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
217
+ SPLIT_PATTERN = /[;,]/.freeze
220
218
 
221
219
  def initialize(header_value)
222
220
  @header_value = header_value
@@ -250,11 +248,25 @@ module HTTPX
250
248
  private
251
249
 
252
250
  def load
253
- m = @header_value.to_s[MIME_TYPE_RE, 1]
254
- m && @mime_type = m.strip.downcase
251
+ if @header_value && !@header_value.empty?
252
+ m, c = @header_value.split(SPLIT_PATTERN, 2)
255
253
 
256
- c = @header_value.to_s[CHARSET_RE, 1]
257
- c && @charset = c.strip.delete('"')
254
+ if m
255
+ @mime_type = m.strip
256
+ @mime_type.downcase
257
+ end
258
+
259
+ if c
260
+ c.strip!
261
+ c.delete_prefix!("charset=")
262
+ c.gsub!(/\A"(.*?)(?<!\\)"\z/) do
263
+ c2 = ::Regexp.last_match(1)
264
+ c2.gsub!(/\\(.)/, '\1')
265
+ c2
266
+ end
267
+ @charset = c
268
+ end
269
+ end
258
270
 
259
271
  @initialized = true
260
272
  end