httpx 1.7.1 → 1.7.3
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.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/doc/release_notes/1_7_2.md +6 -0
- data/doc/release_notes/1_7_3.md +29 -0
- data/lib/httpx/adapters/webmock.rb +3 -4
- data/lib/httpx/connection/http1.rb +0 -1
- data/lib/httpx/connection/http2.rb +41 -30
- data/lib/httpx/connection.rb +18 -4
- data/lib/httpx/plugins/auth/digest.rb +2 -1
- data/lib/httpx/plugins/auth.rb +21 -2
- data/lib/httpx/plugins/cookies/cookie.rb +34 -11
- data/lib/httpx/plugins/cookies/jar.rb +93 -18
- data/lib/httpx/plugins/cookies.rb +7 -3
- data/lib/httpx/plugins/expect.rb +26 -2
- data/lib/httpx/plugins/fiber_concurrency.rb +2 -4
- data/lib/httpx/plugins/follow_redirects.rb +3 -1
- data/lib/httpx/plugins/ntlm_auth.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +19 -19
- data/lib/httpx/plugins/retries.rb +11 -7
- data/lib/httpx/plugins/ssrf_filter.rb +1 -0
- data/lib/httpx/plugins/stream_bidi.rb +17 -1
- data/lib/httpx/request.rb +1 -1
- data/lib/httpx/resolver/resolver.rb +5 -0
- data/lib/httpx/selector.rb +4 -4
- data/lib/httpx/session.rb +6 -5
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +1 -1
- data/sig/connection/http2.rbs +8 -4
- data/sig/connection.rbs +3 -0
- data/sig/plugins/auth.rbs +6 -0
- data/sig/plugins/cookies/cookie.rbs +3 -2
- data/sig/plugins/cookies/jar.rbs +11 -0
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/expect.rbs +17 -2
- data/sig/plugins/proxy/socks4.rbs +4 -0
- data/sig/plugins/rate_limiter.rbs +2 -2
- data/sig/plugins/response_cache.rbs +3 -3
- data/sig/plugins/retries.rbs +17 -13
- data/sig/plugins/stream_bidi.rbs +3 -0
- data/sig/request.rbs +1 -0
- data/sig/resolver/native.rbs +2 -0
- data/sig/resolver/resolver.rbs +2 -2
- data/sig/selector.rbs +4 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54c87d9d8b2be0d12570204fd3c60d37f82127b624dde3a233085d5bcb43778c
|
|
4
|
+
data.tar.gz: 8bfec9fadfe697d083d37a9317e9ec9d6e23016f174bd62cabf24275f18fee79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c425e8714c36bdca0ab5d068157caa94ca591829466ccdb888e903487aaab50c9a8756eaa53f1cd692a109160deca10ec9ce4405a7690f38149a9e95ec9fb17
|
|
7
|
+
data.tar.gz: 20c6ae3067f1fe187402c743fb1741a131ba09e5039ed86e5c5d2eb8c94d590c84644ca3e6ab5fd3948b635d328bba4149df3a1d94dde3dfbf257bd694730275
|
data/README.md
CHANGED
|
@@ -46,7 +46,9 @@ And that's the simplest one there is. But you can also do:
|
|
|
46
46
|
HTTPX.post("http://example.com", form: { user: "john", password: "pass" })
|
|
47
47
|
|
|
48
48
|
http = HTTPX.with(headers: { "x-my-name" => "joe" })
|
|
49
|
-
|
|
49
|
+
File.open("path/to/file") do |file|
|
|
50
|
+
http.patch("http://example.com/file", body: file) # request body is streamed
|
|
51
|
+
end
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
If you want to do some more things with the response, you can get an `HTTPX::Response`:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# 1.7.2
|
|
2
|
+
|
|
3
|
+
## Bugfixes
|
|
4
|
+
|
|
5
|
+
* `:stream_bidi` plugin: when used with the `:retries` plugin, it will skip calling callbacks referencing state from the connection/stream the request was moved from.
|
|
6
|
+
* `:auth` plugin: fix issue causing tokens to be concatenated on non-auth errors.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 1.7.3
|
|
2
|
+
|
|
3
|
+
## Improvements
|
|
4
|
+
|
|
5
|
+
### cookies plugin: Jar as CookieStore
|
|
6
|
+
|
|
7
|
+
While previously an implementation detail, the cookie jar from a `:cookie` plugin-enabled session can now be manipulated by the end user:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
cookies_sess = HTTPX.plugin(:cookies)
|
|
11
|
+
|
|
12
|
+
jar = cookies.make_jar
|
|
13
|
+
|
|
14
|
+
sess = cookies_ses.with(cookies: jar)
|
|
15
|
+
|
|
16
|
+
# perform requests using sess, get/set/delete cookies in jar
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The jar API now closely follows the [Web Cookie Store API](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore), by providing the same set of functions.
|
|
20
|
+
|
|
21
|
+
Some API backwards compatibility is maintained, however since this was an internal implementation detail, this effort isn't meant to be thorough.
|
|
22
|
+
|
|
23
|
+
## Bugfixes
|
|
24
|
+
|
|
25
|
+
* `http-2`: clear buffered data chunks when receiving a `GOAWAY` stream frame; without this, the client kept sending the corresponding `DATA` frames, despite the peer server making it known that it wouldn't process it. While this is valid HTTP/2, this could increase the connection window until a point where it'd go over the max frame size. this issue was observed during large file uploads where the first request could fail and make the client renegotiate.
|
|
26
|
+
* `webmock` adapter: fixed response body length accounting which was making `response.body.empty?` return true for responses with payload.
|
|
27
|
+
* `:rate_limiter` plugin relies on an internal refactoring to be able to wait for the time suggested by the peer server instead of the potentially relying on custom user logic via own `:retry_after`.
|
|
28
|
+
* `:fiber_concurrency`: fix wrong names for native/system resolver overrides.
|
|
29
|
+
* connection: fix for race condition when closing the connection, where the state only transitions to `closed` after checking the connection back in to the pool, potentially corrupting it if another session meanwhile has picked it up and manipulated it.
|
|
@@ -82,6 +82,7 @@ module WebMock
|
|
|
82
82
|
|
|
83
83
|
def mock!
|
|
84
84
|
@mocked = true
|
|
85
|
+
@body.mock!
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
def mocked?
|
|
@@ -90,10 +91,8 @@ module WebMock
|
|
|
90
91
|
end
|
|
91
92
|
|
|
92
93
|
module ResponseBodyMethods
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
super
|
|
94
|
+
def mock!
|
|
95
|
+
@inflaters = nil
|
|
97
96
|
end
|
|
98
97
|
end
|
|
99
98
|
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "http/2"
|
|
5
5
|
|
|
6
|
-
HTTP2::Connection.__send__(:public, :send_buffer) if HTTP2::VERSION < "1.1.1"
|
|
7
|
-
|
|
8
6
|
module HTTPX
|
|
9
7
|
class Connection::HTTP2
|
|
10
8
|
include Callbacks
|
|
@@ -215,9 +213,7 @@ module HTTPX
|
|
|
215
213
|
def handle_stream(stream, request)
|
|
216
214
|
request.on(:refuse, &method(:on_stream_refuse).curry(3)[stream, request])
|
|
217
215
|
stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
|
|
218
|
-
stream.on(:half_close)
|
|
219
|
-
log(level: 2) { "#{stream.id}: waiting for response..." }
|
|
220
|
-
end
|
|
216
|
+
stream.on(:half_close) { on_stream_half_close(stream, request) }
|
|
221
217
|
stream.on(:altsvc, &method(:on_altsvc).curry(2)[request.origin])
|
|
222
218
|
stream.on(:headers, &method(:on_stream_headers).curry(3)[stream, request])
|
|
223
219
|
stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
|
|
@@ -302,7 +298,7 @@ module HTTPX
|
|
|
302
298
|
end
|
|
303
299
|
|
|
304
300
|
log(color: :yellow) do
|
|
305
|
-
h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")
|
|
301
|
+
h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{k == ":status" ? v : log_redact_headers(v)}" }.join("\n")
|
|
306
302
|
end
|
|
307
303
|
_, status = h.shift
|
|
308
304
|
headers = request.options.headers_class.new(h)
|
|
@@ -331,6 +327,16 @@ module HTTPX
|
|
|
331
327
|
stream.close
|
|
332
328
|
end
|
|
333
329
|
|
|
330
|
+
def on_stream_half_close(stream, _request)
|
|
331
|
+
unless stream.send_buffer.empty?
|
|
332
|
+
stream.send_buffer.clear
|
|
333
|
+
stream.data("", end_stream: true)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# TODO: omit log line if response already here
|
|
337
|
+
log(level: 2) { "#{stream.id}: waiting for response..." }
|
|
338
|
+
end
|
|
339
|
+
|
|
334
340
|
def on_stream_close(stream, request, error)
|
|
335
341
|
return if error == :stream_closed && !@streams.key?(request)
|
|
336
342
|
|
|
@@ -404,34 +410,39 @@ module HTTPX
|
|
|
404
410
|
|
|
405
411
|
def on_frame_sent(frame)
|
|
406
412
|
log(level: 2) { "#{frame[:stream]}: frame was sent!" }
|
|
407
|
-
log(level: 2, color: :blue)
|
|
408
|
-
payload =
|
|
409
|
-
case frame[:type]
|
|
410
|
-
when :data
|
|
411
|
-
frame.merge(payload: frame[:payload].bytesize)
|
|
412
|
-
when :headers, :ping
|
|
413
|
-
frame.merge(payload: log_redact_headers(frame[:payload]))
|
|
414
|
-
else
|
|
415
|
-
frame
|
|
416
|
-
end
|
|
417
|
-
"#{frame[:stream]}: #{payload}"
|
|
418
|
-
end
|
|
413
|
+
log(level: 2, color: :blue) { "#{frame[:stream]}: #{frame_with_extra_info(frame)}" }
|
|
419
414
|
end
|
|
420
415
|
|
|
421
416
|
def on_frame_received(frame)
|
|
422
417
|
log(level: 2) { "#{frame[:stream]}: frame was received!" }
|
|
423
|
-
log(level: 2, color: :magenta)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
418
|
+
log(level: 2, color: :magenta) { "#{frame[:stream]}: #{frame_with_extra_info(frame)}" }
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def frame_with_extra_info(frame)
|
|
422
|
+
case frame[:type]
|
|
423
|
+
when :data
|
|
424
|
+
frame.merge(payload: frame[:payload].bytesize)
|
|
425
|
+
when :headers, :ping
|
|
426
|
+
frame.merge(payload: log_redact_headers(frame[:payload]))
|
|
427
|
+
when :window_update
|
|
428
|
+
connection_or_stream = if (id = frame[:stream]).zero?
|
|
429
|
+
@connection
|
|
430
|
+
else
|
|
431
|
+
@streams.each_value.find { |s| s.id == id }
|
|
432
|
+
end
|
|
433
|
+
if connection_or_stream
|
|
434
|
+
frame.merge(
|
|
435
|
+
local_window: connection_or_stream.local_window,
|
|
436
|
+
remote_window: connection_or_stream.remote_window,
|
|
437
|
+
buffered_amount: connection_or_stream.buffered_amount,
|
|
438
|
+
stream_state: connection_or_stream.state,
|
|
439
|
+
)
|
|
440
|
+
else
|
|
441
|
+
frame
|
|
442
|
+
end
|
|
443
|
+
else
|
|
444
|
+
frame
|
|
445
|
+
end.merge(connection_state: @connection.state)
|
|
435
446
|
end
|
|
436
447
|
|
|
437
448
|
def on_altsvc(origin, frame)
|
data/lib/httpx/connection.rb
CHANGED
|
@@ -227,6 +227,9 @@ module HTTPX
|
|
|
227
227
|
consume
|
|
228
228
|
end
|
|
229
229
|
nil
|
|
230
|
+
rescue IOError => e
|
|
231
|
+
@write_buffer.clear
|
|
232
|
+
on_io_error(e)
|
|
230
233
|
rescue StandardError => e
|
|
231
234
|
@write_buffer.clear
|
|
232
235
|
on_error(e)
|
|
@@ -375,6 +378,11 @@ module HTTPX
|
|
|
375
378
|
current_session.deselect_connection(self, current_selector, @cloned)
|
|
376
379
|
end
|
|
377
380
|
|
|
381
|
+
def on_io_error(e)
|
|
382
|
+
on_error(e)
|
|
383
|
+
force_close(true)
|
|
384
|
+
end
|
|
385
|
+
|
|
378
386
|
def on_error(error, request = nil)
|
|
379
387
|
if error.is_a?(OperationTimeoutError)
|
|
380
388
|
|
|
@@ -493,7 +501,7 @@ module HTTPX
|
|
|
493
501
|
# flush as many bytes as the sockets allow.
|
|
494
502
|
#
|
|
495
503
|
loop do
|
|
496
|
-
# buffer has been
|
|
504
|
+
# buffer has been drained, mark and exit the write loop.
|
|
497
505
|
if @write_buffer.empty?
|
|
498
506
|
# we only mark as drained on the first loop
|
|
499
507
|
write_drained = write_drained.nil? && @inflight.positive?
|
|
@@ -586,6 +594,8 @@ module HTTPX
|
|
|
586
594
|
# back to the pending list before the parser is reset.
|
|
587
595
|
@inflight -= parser_pending_requests.size
|
|
588
596
|
@pending.unshift(*parser_pending_requests)
|
|
597
|
+
|
|
598
|
+
parser.pending.clear
|
|
589
599
|
end
|
|
590
600
|
|
|
591
601
|
def build_parser(protocol = @io.protocol)
|
|
@@ -721,8 +731,6 @@ module HTTPX
|
|
|
721
731
|
|
|
722
732
|
# do not deactivate connection in use
|
|
723
733
|
return if @inflight.positive? || @parser.waiting_for_ping?
|
|
724
|
-
|
|
725
|
-
disconnect
|
|
726
734
|
when :closing
|
|
727
735
|
return unless connecting? || @state == :open
|
|
728
736
|
|
|
@@ -740,8 +748,9 @@ module HTTPX
|
|
|
740
748
|
return unless @write_buffer.empty?
|
|
741
749
|
|
|
742
750
|
purge_after_closed
|
|
743
|
-
disconnect if @pending.empty?
|
|
744
751
|
|
|
752
|
+
# TODO: should this raise an error instead?
|
|
753
|
+
return unless @pending.empty?
|
|
745
754
|
when :already_open
|
|
746
755
|
nextstate = :open
|
|
747
756
|
# the first check for given io readiness must still use a timeout.
|
|
@@ -758,6 +767,11 @@ module HTTPX
|
|
|
758
767
|
end
|
|
759
768
|
log(level: 3) { "#{@state} -> #{nextstate}" }
|
|
760
769
|
@state = nextstate
|
|
770
|
+
# post state change
|
|
771
|
+
case nextstate
|
|
772
|
+
when :closed, :inactive
|
|
773
|
+
disconnect
|
|
774
|
+
end
|
|
761
775
|
end
|
|
762
776
|
|
|
763
777
|
def close_sibling
|
data/lib/httpx/plugins/auth.rb
CHANGED
|
@@ -69,7 +69,7 @@ module HTTPX
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
71
|
def send_request(request, *)
|
|
72
|
-
return super if @skip_auth_header_value
|
|
72
|
+
return super if @skip_auth_header_value || request.authorized?
|
|
73
73
|
|
|
74
74
|
@auth_header_value ||= generate_auth_token
|
|
75
75
|
|
|
@@ -92,12 +92,31 @@ module HTTPX
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
module RequestMethods
|
|
95
|
+
def initialize(*)
|
|
96
|
+
super
|
|
97
|
+
@auth_token_value = nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def authorized?
|
|
101
|
+
!@auth_token_value.nil?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def unauthorize!
|
|
105
|
+
return unless (auth_value = @auth_token_value)
|
|
106
|
+
|
|
107
|
+
@headers.get("authorization").delete(auth_value)
|
|
108
|
+
|
|
109
|
+
@auth_token_value = nil
|
|
110
|
+
end
|
|
111
|
+
|
|
95
112
|
def authorize(auth_value)
|
|
96
113
|
if (auth_type = @options.auth_header_type)
|
|
97
114
|
auth_value = "#{auth_type} #{auth_value}"
|
|
98
115
|
end
|
|
99
116
|
|
|
100
117
|
@headers.add("authorization", auth_value)
|
|
118
|
+
|
|
119
|
+
@auth_token_value = auth_value
|
|
101
120
|
end
|
|
102
121
|
end
|
|
103
122
|
|
|
@@ -119,7 +138,7 @@ module HTTPX
|
|
|
119
138
|
return unless auth_error?(response, request.options) ||
|
|
120
139
|
(@options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response))
|
|
121
140
|
|
|
122
|
-
request.
|
|
141
|
+
request.unauthorize!
|
|
123
142
|
@auth_header_value = generate_auth_token
|
|
124
143
|
end
|
|
125
144
|
|
|
@@ -14,12 +14,14 @@ module HTTPX
|
|
|
14
14
|
|
|
15
15
|
attr_reader :domain, :path, :name, :value, :created_at
|
|
16
16
|
|
|
17
|
+
# assigns a new +path+ to this cookie.
|
|
17
18
|
def path=(path)
|
|
18
19
|
path = String(path)
|
|
20
|
+
@for_domain = false
|
|
19
21
|
@path = path.start_with?("/") ? path : "/"
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
#
|
|
24
|
+
# assigns a new +domain+ to this cookie.
|
|
23
25
|
def domain=(domain)
|
|
24
26
|
domain = String(domain)
|
|
25
27
|
|
|
@@ -37,6 +39,13 @@ module HTTPX
|
|
|
37
39
|
@domain = @domain_name.hostname
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
# checks whether +other+ is the same cookie, i.e. name, value, domain and path are
|
|
43
|
+
# the same.
|
|
44
|
+
def ==(other)
|
|
45
|
+
@name == other.name && @value == other.value &&
|
|
46
|
+
@path == other.path && @domain == other.domain
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
# Compares the cookie with another. When there are many cookies with
|
|
41
50
|
# the same name for a URL, the value of the smallest must be used.
|
|
42
51
|
def <=>(other)
|
|
@@ -47,11 +56,29 @@ module HTTPX
|
|
|
47
56
|
(@created_at <=> other.created_at).nonzero? || 0
|
|
48
57
|
end
|
|
49
58
|
|
|
59
|
+
def match?(name_or_options)
|
|
60
|
+
case name_or_options
|
|
61
|
+
when String
|
|
62
|
+
@name == name_or_options
|
|
63
|
+
when Hash, Array
|
|
64
|
+
name_or_options.all? { |k, v| respond_to?(k) && send(k) == v }
|
|
65
|
+
else
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
50
70
|
class << self
|
|
51
71
|
def new(cookie, *args)
|
|
52
|
-
|
|
72
|
+
case cookie
|
|
73
|
+
when self
|
|
74
|
+
cookie
|
|
75
|
+
when Array, Hash
|
|
76
|
+
options = Hash[cookie] #: cookie_attributes
|
|
77
|
+
super(options[:name], options[:value], options)
|
|
78
|
+
else
|
|
53
79
|
|
|
54
|
-
|
|
80
|
+
super
|
|
81
|
+
end
|
|
55
82
|
end
|
|
56
83
|
|
|
57
84
|
# Tests if +target_path+ is under +base_path+ as described in RFC
|
|
@@ -84,16 +111,12 @@ module HTTPX
|
|
|
84
111
|
end
|
|
85
112
|
end
|
|
86
113
|
|
|
87
|
-
def initialize(arg,
|
|
114
|
+
def initialize(arg, value, attrs = nil)
|
|
88
115
|
@created_at = Time.now
|
|
89
116
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@name = arg
|
|
94
|
-
@value, attr_hash = attrs
|
|
95
|
-
attr_hash = Hash.try_convert(attr_hash)
|
|
96
|
-
end
|
|
117
|
+
@name = arg
|
|
118
|
+
@value = value
|
|
119
|
+
attr_hash = Hash.try_convert(attrs)
|
|
97
120
|
|
|
98
121
|
attr_hash.each do |key, val|
|
|
99
122
|
key = key.downcase.tr("-", "_").to_sym unless key.is_a?(Symbol)
|
|
@@ -4,7 +4,12 @@ module HTTPX
|
|
|
4
4
|
module Plugins::Cookies
|
|
5
5
|
# The Cookie Jar
|
|
6
6
|
#
|
|
7
|
-
# It
|
|
7
|
+
# It stores and manages cookies for a session, such as i.e. evicting when expired, access methods, or
|
|
8
|
+
# initialization from parsing `Set-Cookie` HTTP header values.
|
|
9
|
+
#
|
|
10
|
+
# It closely follows the [CookieStore API](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore),
|
|
11
|
+
# by implementing the same methods, with a few specific conveniences for this non-browser manipulation use-case.
|
|
12
|
+
#
|
|
8
13
|
class Jar
|
|
9
14
|
using URIExtensions
|
|
10
15
|
|
|
@@ -12,10 +17,14 @@ module HTTPX
|
|
|
12
17
|
|
|
13
18
|
def initialize_dup(orig)
|
|
14
19
|
super
|
|
20
|
+
@mtx = orig.instance_variable_get(:@mtx).dup
|
|
15
21
|
@cookies = orig.instance_variable_get(:@cookies).dup
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
# initializes the cookie store, either empty, or with whatever is passed as +cookies+, which
|
|
25
|
+
# can be an array of HTTPX::Plugins::Cookies::Cookie objects or hashes-or-tuples of cookie attributes.
|
|
18
26
|
def initialize(cookies = nil)
|
|
27
|
+
@mtx = Thread::Mutex.new
|
|
19
28
|
@cookies = []
|
|
20
29
|
|
|
21
30
|
cookies.each do |elem|
|
|
@@ -32,48 +41,106 @@ module HTTPX
|
|
|
32
41
|
end if cookies
|
|
33
42
|
end
|
|
34
43
|
|
|
44
|
+
# parses the `Set-Cookie` header value as +set_cookie+ and does the corresponding updates.
|
|
35
45
|
def parse(set_cookie)
|
|
36
46
|
SetCookieParser.call(set_cookie) do |name, value, attrs|
|
|
37
|
-
|
|
47
|
+
set(Cookie.new(name, value, attrs))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# returns the first HTTPX::Plugins::Cookie::Cookie instance in the store which matches either the name
|
|
52
|
+
# (when String) or all attributes (when a Hash or array of tuples) passed to +name_or_options+
|
|
53
|
+
def get(name_or_options)
|
|
54
|
+
each.find { |ck| ck.match?(name_or_options) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# returns all HTTPX::Plugins::Cookie::Cookie instances in the store which match either the name
|
|
58
|
+
# (when String) or all attributes (when a Hash or array of tuples) passed to +name_or_options+
|
|
59
|
+
def get_all(name_or_options)
|
|
60
|
+
each.select { |ck| ck.match?(name_or_options) } # rubocop:disable Style/SelectByRegexp
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# when +name+ is a HTTPX::Plugins::Cookie::Cookie, it stores it internally; when +name+ is a String,
|
|
64
|
+
# it creates a cookie with it and the value-or-attributes passed to +value_or_options+.
|
|
65
|
+
|
|
66
|
+
# optionally, +name+ can also be the attributes hash-or-array as long it contains a <tt>:name</tt> field).
|
|
67
|
+
def set(name, value_or_options = nil)
|
|
68
|
+
cookie = case name
|
|
69
|
+
when Cookie
|
|
70
|
+
raise ArgumentError, "there should not be a second argument" if value_or_options
|
|
71
|
+
|
|
72
|
+
name
|
|
73
|
+
when Array, Hash
|
|
74
|
+
raise ArgumentError, "there should not be a second argument" if value_or_options
|
|
75
|
+
|
|
76
|
+
Cookie.new(name)
|
|
77
|
+
else
|
|
78
|
+
raise ArgumentError, "the second argument is required" unless value_or_options
|
|
79
|
+
|
|
80
|
+
Cookie.new(name, value_or_options)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
synchronize do
|
|
84
|
+
# If the user agent receives a new cookie with the same cookie-name, domain-value, and path-value
|
|
85
|
+
# as a cookie that it has already stored, the existing cookie is evicted and replaced with the new cookie.
|
|
86
|
+
@cookies.delete_if { |ck| ck.name == cookie.name && ck.domain == cookie.domain && ck.path == cookie.path }
|
|
87
|
+
|
|
88
|
+
@cookies << cookie
|
|
38
89
|
end
|
|
39
90
|
end
|
|
40
91
|
|
|
92
|
+
# @deprecated
|
|
41
93
|
def add(cookie, path = nil)
|
|
94
|
+
warn "DEPRECATION WARNING: calling `##{__method__}` is deprecated. Use `#set` instead."
|
|
42
95
|
c = cookie.dup
|
|
43
|
-
|
|
44
96
|
c.path = path if path && c.path == "/"
|
|
97
|
+
set(c)
|
|
98
|
+
end
|
|
45
99
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
100
|
+
# deletes all cookies in the store which match either the name (when String) or all attributes (when a Hash
|
|
101
|
+
# or array of tuples) passed to +name_or_options+.
|
|
102
|
+
#
|
|
103
|
+
# alternatively, of +name_or_options+ is an instance of HTTPX::Plugins::Cookies::Cookiem, it deletes it from the store.
|
|
104
|
+
def delete(name_or_options)
|
|
105
|
+
synchronize do
|
|
106
|
+
case name_or_options
|
|
107
|
+
when Cookie
|
|
108
|
+
@cookies.delete(name_or_options)
|
|
109
|
+
else
|
|
110
|
+
@cookies.delete_if { |ck| ck.match?(name_or_options) }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
51
113
|
end
|
|
52
114
|
|
|
115
|
+
# returns the list of valid cookies which matdh the domain and path from the URI object passed to +uri+.
|
|
53
116
|
def [](uri)
|
|
54
117
|
each(uri).sort
|
|
55
118
|
end
|
|
56
119
|
|
|
120
|
+
# enumerates over all stored cookies. if +uri+ is passed, it'll filter out expired cookies and
|
|
121
|
+
# only yield cookies which match its domain and path.
|
|
57
122
|
def each(uri = nil, &blk)
|
|
58
123
|
return enum_for(__method__, uri) unless blk
|
|
59
124
|
|
|
60
|
-
return @cookies.each(&blk) unless uri
|
|
125
|
+
return synchronize { @cookies.each(&blk) } unless uri
|
|
61
126
|
|
|
62
127
|
now = Time.now
|
|
63
128
|
tpath = uri.path
|
|
64
129
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
130
|
+
synchronize do
|
|
131
|
+
@cookies.delete_if do |cookie|
|
|
132
|
+
if cookie.expired?(now)
|
|
133
|
+
true
|
|
134
|
+
else
|
|
135
|
+
yield cookie if cookie.valid_for_uri?(uri) && Cookie.path_match?(cookie.path, tpath)
|
|
136
|
+
false
|
|
137
|
+
end
|
|
71
138
|
end
|
|
72
139
|
end
|
|
73
140
|
end
|
|
74
141
|
|
|
75
142
|
def merge(other)
|
|
76
|
-
|
|
143
|
+
jar_dup = dup
|
|
77
144
|
|
|
78
145
|
other.each do |elem|
|
|
79
146
|
cookie = case elem
|
|
@@ -85,10 +152,18 @@ module HTTPX
|
|
|
85
152
|
Cookie.new(elem)
|
|
86
153
|
end
|
|
87
154
|
|
|
88
|
-
|
|
155
|
+
jar_dup.set(cookie)
|
|
89
156
|
end
|
|
90
157
|
|
|
91
|
-
|
|
158
|
+
jar_dup
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def synchronize(&block)
|
|
164
|
+
return yield if @mtx.owned?
|
|
165
|
+
|
|
166
|
+
@mtx.synchronize(&block)
|
|
92
167
|
end
|
|
93
168
|
end
|
|
94
169
|
end
|
|
@@ -7,8 +7,6 @@ module HTTPX
|
|
|
7
7
|
#
|
|
8
8
|
# This plugin implements a persistent cookie jar for the duration of a session.
|
|
9
9
|
#
|
|
10
|
-
# It also adds a *#cookies* helper, so that you can pre-fill the cookies of a session.
|
|
11
|
-
#
|
|
12
10
|
# https://gitlab.com/os85/httpx/wikis/Cookies
|
|
13
11
|
#
|
|
14
12
|
module Cookies
|
|
@@ -46,6 +44,12 @@ module HTTPX
|
|
|
46
44
|
request
|
|
47
45
|
end
|
|
48
46
|
|
|
47
|
+
# factory method to return a Jar to the user, which can then manipulate
|
|
48
|
+
# externally to the session.
|
|
49
|
+
def make_jar(*args)
|
|
50
|
+
Jar.new(*args)
|
|
51
|
+
end
|
|
52
|
+
|
|
49
53
|
private
|
|
50
54
|
|
|
51
55
|
def set_request_callbacks(request)
|
|
@@ -96,7 +100,7 @@ module HTTPX
|
|
|
96
100
|
cookies.each do |ck|
|
|
97
101
|
ck.split(/ *; */).each do |cookie|
|
|
98
102
|
name, value = cookie.split("=", 2)
|
|
99
|
-
jar.
|
|
103
|
+
jar.set(name, value)
|
|
100
104
|
end
|
|
101
105
|
end
|
|
102
106
|
end
|
data/lib/httpx/plugins/expect.rb
CHANGED
|
@@ -9,10 +9,34 @@ module HTTPX
|
|
|
9
9
|
#
|
|
10
10
|
module Expect
|
|
11
11
|
EXPECT_TIMEOUT = 2
|
|
12
|
+
NOEXPECT_STORE_MUTEX = Thread::Mutex.new
|
|
13
|
+
|
|
14
|
+
class Store
|
|
15
|
+
def initialize
|
|
16
|
+
@store = []
|
|
17
|
+
@mutex = Thread::Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def include?(host)
|
|
21
|
+
@mutex.synchronize { @store.include?(host) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add(host)
|
|
25
|
+
@mutex.synchronize { @store << host }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete(host)
|
|
29
|
+
@mutex.synchronize { @store.delete(host) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
12
32
|
|
|
13
33
|
class << self
|
|
14
34
|
def no_expect_store
|
|
15
|
-
|
|
35
|
+
return Ractor.store_if_absent(:httpx_no_expect_store) { Store.new } if Utils.in_ractor?
|
|
36
|
+
|
|
37
|
+
@no_expect_store ||= NOEXPECT_STORE_MUTEX.synchronize do
|
|
38
|
+
@no_expect_store || Store.new
|
|
39
|
+
end
|
|
16
40
|
end
|
|
17
41
|
|
|
18
42
|
def extra_options(options)
|
|
@@ -89,7 +113,7 @@ module HTTPX
|
|
|
89
113
|
set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
|
|
90
114
|
# expect timeout expired
|
|
91
115
|
if request.state == :expect && !request.expects?
|
|
92
|
-
Expect.no_expect_store
|
|
116
|
+
Expect.no_expect_store.add(request.origin)
|
|
93
117
|
request.headers.delete("expect")
|
|
94
118
|
consume
|
|
95
119
|
end
|
|
@@ -160,9 +160,7 @@ module HTTPX
|
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
-
module
|
|
164
|
-
private
|
|
165
|
-
|
|
163
|
+
module ResolverNativeMethods
|
|
166
164
|
def calculate_interests
|
|
167
165
|
return if @queries.empty?
|
|
168
166
|
|
|
@@ -172,7 +170,7 @@ module HTTPX
|
|
|
172
170
|
end
|
|
173
171
|
end
|
|
174
172
|
|
|
175
|
-
module
|
|
173
|
+
module ResolverSystemMethods
|
|
176
174
|
def interests
|
|
177
175
|
return unless @queries.any? { |_, conn| conn.current_context? }
|
|
178
176
|
|