patient_http 1.5.0 → 1.6.0
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/CHANGELOG.md +16 -0
- data/README.md +55 -3
- data/VERSION +1 -1
- data/lib/patient_http/configuration.rb +33 -0
- data/lib/patient_http/outgoing_request.rb +1 -1
- data/lib/patient_http/processor.rb +1 -1
- data/lib/patient_http/redirect_helper.rb +84 -1
- data/lib/patient_http/request.rb +41 -4
- data/lib/patient_http/request_helper.rb +49 -5
- data/lib/patient_http/request_task.rb +24 -11
- data/lib/patient_http/request_template.rb +39 -2
- data/lib/patient_http/response_reader.rb +8 -2
- data/lib/patient_http/synchronous_executor.rb +2 -44
- data/lib/patient_http.rb +36 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54ede397ae79574e7dd597ea642b4d039877d5d642ba49c68b76219b8b1c4a32
|
|
4
|
+
data.tar.gz: 83bb067b78912753791e6002abb3470ca70936f5fe862fc65ee65d7786e1ac53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0302b8cdf7042fa545e49ab1ce565d5f7d1fd388a8413bc8a19249477d46d2fc9f732142d75afbb6cc52d0dd7a228e01c6f0c1cbaea2f7b151568472ccfaecb4
|
|
7
|
+
data.tar.gz: f418c2d7b5b48ba81ac73de35533143d7e99c75fb108a619f9cf9a55ca204f56f42ffec86ee4fc6d2796730703aad488014955603f87fc94ccf3e66d6b7b8593
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 1.6.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `Configuration#follow_method_changing_redirects` and `Request#follow_method_changing_redirects` (default true): when false, a redirect that would change the HTTP method (such as POST to GET on a 301, 302, or 303) is not followed and the redirect response is delivered to the callback instead. Redirects that preserve the method are still followed. The request setting overrides the configuration.
|
|
12
|
+
- `Configuration#redirect_strip_headers` and `Request#redirect_strip_headers`: header names (matched case insensitively) that are always removed from redirected requests, so sensitive headers are never sent to a redirect target. Request-level names are applied in addition to the configured ones and survive serialization.
|
|
13
|
+
- `PatientHttp.request`, `RequestHelper#async_request`, and `RequestTemplate#request` (and their `get`, `post`, and other method helpers) accept `follow_method_changing_redirects:` and `redirect_strip_headers:` and pass them to the `Request`.
|
|
14
|
+
- `HEAD` and `QUERY` HTTP methods are supported by `Request`, `RequestTemplate`, `RequestHelper` (`async_head`, `async_query`), and `PatientHttp.head` / `PatientHttp.query`. `HEAD` requests cannot carry a body. The response size limit is not applied to the `Content-Length` of a `HEAD` response because no body is transferred.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The HTTP method used when following a redirect now follows RFC 9110. On 301 and 302 responses only `POST` is changed to `GET`; `HEAD`, `PUT`, `PATCH`, `DELETE`, and `QUERY` are re-sent unchanged with their body. On 303 responses `GET` and `HEAD` are preserved and every other method becomes `GET`. Previously every method was changed to `GET` on 301, 302, and 303.
|
|
19
|
+
- When a redirect changes the method and drops the body, the `Content-Type`, `Content-Length`, `Content-Encoding`, `Content-Language`, and `Content-Location` headers are removed from the redirected request as well.
|
|
20
|
+
- A 300 Multiple Choices response with a `Location` header is now followed, preserving the method and body. A 300 response without `Location` is delivered as a response, as before.
|
|
21
|
+
- `RequestTask#redirect_task` accepts a `strip_headers:` option with the configured header names to remove.
|
|
22
|
+
|
|
7
23
|
## 1.5.0
|
|
8
24
|
|
|
9
25
|
### Added
|
data/README.md
CHANGED
|
@@ -163,7 +163,7 @@ get_request = template.get("/users/123")
|
|
|
163
163
|
post_request = template.post("/users", json: {name: "John"})
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
Templates support all HTTP methods (`get`, `post`, `put`, `patch`, `delete`) and handle URL joining, header merging, and query parameter encoding.
|
|
166
|
+
Templates support all HTTP methods (`get`, `head`, `post`, `put`, `patch`, `delete`, `query`) and handle URL joining, header merging, and query parameter encoding.
|
|
167
167
|
|
|
168
168
|
## Standard Interface
|
|
169
169
|
|
|
@@ -188,7 +188,7 @@ PatientHttp.register_handler do |request:, callback:, callback_args: nil, raise_
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
# Now you can make requests directly through the PatientHttp interface with the .request,
|
|
191
|
-
# .get, .post, .patch, .put, and .
|
|
191
|
+
# .get, .head, .post, .patch, .put, .delete, and .query class methods:
|
|
192
192
|
PatientHttp.get(
|
|
193
193
|
"https://api.example.com/users/123",
|
|
194
194
|
callback: FetchUserCallback,
|
|
@@ -229,7 +229,7 @@ Use `PatientHttp::RequestHelper` when you want a simple API for creating and dis
|
|
|
229
229
|
1. Register a request handler with `PatientHttp.register_handler` that defines how requests are dispatched to your job queue or background processing system.
|
|
230
230
|
2. Include `PatientHttp::RequestHelper` in your class.
|
|
231
231
|
3. Optionally define a `request_template` for shared `base_url`, headers, and timeout.
|
|
232
|
-
4. Call `async_get`, `async_post`, `async_put`, `async_patch`, `async_delete`, or `async_request`.
|
|
232
|
+
4. Call `async_get`, `async_head`, `async_post`, `async_put`, `async_patch`, `async_delete`, `async_query`, or `async_request`.
|
|
233
233
|
|
|
234
234
|
```ruby
|
|
235
235
|
class ApiClient
|
|
@@ -618,6 +618,49 @@ If a request references a preprocessor name that is not registered, a `PatientHt
|
|
|
618
618
|
|
|
619
619
|
When redirects are followed, preprocessors are re-run against each redirect URL so signatures stay valid. On cross-origin redirects they are dropped entirely, consistent with the stripping of `Authorization` and `Cookie` headers, so signed credentials are never sent to an unexpected origin.
|
|
620
620
|
|
|
621
|
+
## Redirects
|
|
622
|
+
|
|
623
|
+
Redirect responses (300, 301, 302, 303, 307, and 308) with a `Location` header are followed automatically, up to `max_redirects` hops. A 300 response is followed only when the server names a preferred choice in `Location`. Redirect loops raise `RecursiveRedirectError` and exceeding the limit raises `TooManyRedirectsError`. Any redirect that is not followed is delivered to the callback as a normal response.
|
|
624
|
+
|
|
625
|
+
The HTTP method of the redirected request follows RFC 9110:
|
|
626
|
+
|
|
627
|
+
| Status | Method |
|
|
628
|
+
| --- | --- |
|
|
629
|
+
| 301, 302 | `POST` becomes `GET` and the body is dropped. Other methods (including `HEAD`, `PUT`, `DELETE`, and `QUERY`) are preserved with their body. |
|
|
630
|
+
| 303 | `GET` and `HEAD` are preserved. Every other method becomes `GET` and the body is dropped. |
|
|
631
|
+
| 300, 307, 308 | The method and body are preserved. |
|
|
632
|
+
|
|
633
|
+
The QUERY specification states that the POST-to-GET exception on 301 and 302 does not apply to `QUERY`, so a redirected `QUERY` is re-sent as a `QUERY` with its body, and a 303 turns it into a `GET`.
|
|
634
|
+
|
|
635
|
+
### Preventing method changes
|
|
636
|
+
|
|
637
|
+
Set `follow_method_changing_redirects: false` to stop following redirects that would change the HTTP method. A `POST` that receives a 302 then completes with the 302 response instead of being retried as a `GET`. Redirects that preserve the method (a `PUT` on a 301, or any method on a 307) are still followed. The option can be set on the `Configuration` or on a single `Request`; the request value wins when both are set.
|
|
638
|
+
|
|
639
|
+
```ruby
|
|
640
|
+
config = PatientHttp::Configuration.new(follow_method_changing_redirects: false)
|
|
641
|
+
|
|
642
|
+
# Or per request
|
|
643
|
+
request = PatientHttp::Request.new(:post, "https://api.example.com/submit", body: payload, follow_method_changing_redirects: false)
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Stripping headers on redirects
|
|
647
|
+
|
|
648
|
+
`Authorization` and `Cookie` headers are always removed on cross-origin redirects. To make sure other sensitive headers are never sent to a redirect target, list them in `redirect_strip_headers`. Header names are matched case insensitively. Listed headers are removed from every redirected request, same-origin or not.
|
|
649
|
+
|
|
650
|
+
```ruby
|
|
651
|
+
config = PatientHttp::Configuration.new(redirect_strip_headers: ["X-Api-Key", "X-Internal-Token"])
|
|
652
|
+
|
|
653
|
+
# Or per request; these are stripped in addition to the configured headers
|
|
654
|
+
request = PatientHttp::Request.new(:get, "https://api.example.com/data", headers: headers, redirect_strip_headers: "X-Signature")
|
|
655
|
+
|
|
656
|
+
# The same options are accepted by PatientHttp.request, the async_* helpers, and RequestTemplate
|
|
657
|
+
PatientHttp.get("https://api.example.com/data", callback: FetchCallback, redirect_strip_headers: "X-Signature")
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Per-request header names survive serialization into the job queue, so they apply no matter which process follows the redirect.
|
|
661
|
+
|
|
662
|
+
Stripping applies to the headers set on the request. Preprocessors run again on each same-origin redirect and can add headers after the strip, so a header that a preprocessor sets is sent to the redirect target. When a redirect changes the method and drops the body, the headers that describe the body (`Content-Type`, `Content-Length`, `Content-Encoding`, `Content-Language`, and `Content-Location`) are removed as well.
|
|
663
|
+
|
|
621
664
|
## Troubleshooting
|
|
622
665
|
|
|
623
666
|
### Warning: `ThreadError: Attempt to unlock a mutex which is not locked`
|
|
@@ -660,6 +703,15 @@ config = PatientHttp::Configuration.new(
|
|
|
660
703
|
# Maximum redirects to follow (default: 5, 0 disables)
|
|
661
704
|
max_redirects: 5,
|
|
662
705
|
|
|
706
|
+
# Follow redirects that must change the HTTP method, such as POST to GET on
|
|
707
|
+
# a 302 (default: true). When false, those requests receive the redirect response.
|
|
708
|
+
follow_method_changing_redirects: true,
|
|
709
|
+
|
|
710
|
+
# Header names (case insensitive) always stripped from redirected requests
|
|
711
|
+
# (default: []). Authorization and Cookie are always stripped on cross-origin
|
|
712
|
+
# redirects.
|
|
713
|
+
redirect_strip_headers: ["X-Api-Key", "X-Internal-Token"],
|
|
714
|
+
|
|
663
715
|
# Maximum number of hosts to maintain persistent connections for (default: 100)
|
|
664
716
|
connection_pool_size: 100,
|
|
665
717
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.6.0
|
|
@@ -43,6 +43,15 @@ module PatientHttp
|
|
|
43
43
|
# @return [Integer] Maximum number of redirects to follow (0 disables redirects)
|
|
44
44
|
attr_reader :max_redirects
|
|
45
45
|
|
|
46
|
+
# @return [Boolean] Whether a redirect that requires changing the HTTP method
|
|
47
|
+
# (for example POST to GET on a 302) may be followed. When false, such a
|
|
48
|
+
# redirect response is returned as the result instead of being followed.
|
|
49
|
+
attr_reader :follow_method_changing_redirects
|
|
50
|
+
|
|
51
|
+
# @return [Array<String>] Lowercase header names that are always stripped from
|
|
52
|
+
# redirected requests
|
|
53
|
+
attr_reader :redirect_strip_headers
|
|
54
|
+
|
|
46
55
|
# @return [Integer] This is the maximum number of hosts for which connections
|
|
47
56
|
# will be kept alive for at one time.
|
|
48
57
|
attr_reader :connection_pool_size
|
|
@@ -73,6 +82,12 @@ module PatientHttp
|
|
|
73
82
|
# @param user_agent [String, nil] Default User-Agent header value
|
|
74
83
|
# @param raise_error_responses [Boolean] Whether to raise HttpError for non-2xx responses by default
|
|
75
84
|
# @param max_redirects [Integer] Maximum number of redirects to follow (0 disables redirects)
|
|
85
|
+
# @param follow_method_changing_redirects [Boolean] Whether to follow a redirect that requires changing the
|
|
86
|
+
# HTTP method, such as POST to GET on a 301, 302, or 303 response. When false, requests
|
|
87
|
+
# whose method would change do not follow the redirect and receive the redirect response.
|
|
88
|
+
# @param redirect_strip_headers [String, Array<String>] Header names (case insensitive)
|
|
89
|
+
# that are always stripped from redirected requests, so sensitive headers are never
|
|
90
|
+
# sent to a redirect target
|
|
76
91
|
# @param connection_pool_size [Integer] Maximum number of host clients to pool
|
|
77
92
|
# @param connection_timeout [Numeric, nil] Connection timeout in seconds
|
|
78
93
|
# @param proxy_url [String, nil] HTTP/HTTPS proxy URL (supports authentication)
|
|
@@ -93,6 +108,8 @@ module PatientHttp
|
|
|
93
108
|
user_agent: "PatientHttp",
|
|
94
109
|
raise_error_responses: false,
|
|
95
110
|
max_redirects: 5,
|
|
111
|
+
follow_method_changing_redirects: true,
|
|
112
|
+
redirect_strip_headers: [],
|
|
96
113
|
connection_pool_size: 100,
|
|
97
114
|
connection_timeout: nil,
|
|
98
115
|
proxy_url: nil,
|
|
@@ -126,6 +143,8 @@ module PatientHttp
|
|
|
126
143
|
self.user_agent = user_agent
|
|
127
144
|
self.raise_error_responses = raise_error_responses
|
|
128
145
|
self.max_redirects = max_redirects
|
|
146
|
+
self.follow_method_changing_redirects = follow_method_changing_redirects
|
|
147
|
+
self.redirect_strip_headers = redirect_strip_headers
|
|
129
148
|
self.connection_pool_size = connection_pool_size
|
|
130
149
|
self.connection_timeout = connection_timeout
|
|
131
150
|
self.proxy_url = proxy_url
|
|
@@ -186,6 +205,18 @@ module PatientHttp
|
|
|
186
205
|
@max_redirects = value
|
|
187
206
|
end
|
|
188
207
|
|
|
208
|
+
def follow_method_changing_redirects=(value)
|
|
209
|
+
unless value == true || value == false
|
|
210
|
+
raise ArgumentError.new("follow_method_changing_redirects must be true or false, got: #{value.inspect}")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
@follow_method_changing_redirects = value
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def redirect_strip_headers=(value)
|
|
217
|
+
@redirect_strip_headers = RedirectHelper.normalize_header_names(value)
|
|
218
|
+
end
|
|
219
|
+
|
|
189
220
|
def connection_pool_size=(value)
|
|
190
221
|
validate_positive_integer(:connection_pool_size, value)
|
|
191
222
|
@connection_pool_size = value
|
|
@@ -425,6 +456,8 @@ module PatientHttp
|
|
|
425
456
|
"user_agent" => user_agent,
|
|
426
457
|
"raise_error_responses" => raise_error_responses,
|
|
427
458
|
"max_redirects" => max_redirects,
|
|
459
|
+
"follow_method_changing_redirects" => follow_method_changing_redirects,
|
|
460
|
+
"redirect_strip_headers" => redirect_strip_headers,
|
|
428
461
|
"connection_pool_size" => connection_pool_size,
|
|
429
462
|
"connection_timeout" => connection_timeout,
|
|
430
463
|
"proxy_url" => proxy_url,
|
|
@@ -12,7 +12,7 @@ module PatientHttp
|
|
|
12
12
|
#
|
|
13
13
|
# @see Configuration#register_preprocessor
|
|
14
14
|
class OutgoingRequest
|
|
15
|
-
# @return [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
|
|
15
|
+
# @return [Symbol] HTTP method (:get, :head, :post, :put, :patch, :delete, :query)
|
|
16
16
|
attr_reader :http_method
|
|
17
17
|
|
|
18
18
|
# @return [String] the request URL with any secret query params already resolved
|
|
@@ -809,7 +809,7 @@ module PatientHttp
|
|
|
809
809
|
# Create the redirect task, then atomically claim the original (remove it
|
|
810
810
|
# from in-flight) and enqueue the redirect. If the claim fails the
|
|
811
811
|
# shutdown sequence already re-enqueued the original, so drop the redirect.
|
|
812
|
-
redirect_task = task
|
|
812
|
+
redirect_task = build_redirect_task(task, response_data)
|
|
813
813
|
|
|
814
814
|
begin
|
|
815
815
|
claimed = announce_and_enqueue(redirect_task) do
|
|
@@ -2,10 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
4
|
# Shared redirect-checking logic used by both the async Processor
|
|
5
|
-
# and the SynchronousExecutor.
|
|
5
|
+
# and the SynchronousExecutor. Including classes must expose the
|
|
6
|
+
# active {Configuration} as `@config`.
|
|
6
7
|
#
|
|
7
8
|
# @api private
|
|
8
9
|
module RedirectHelper
|
|
10
|
+
class << self
|
|
11
|
+
# Determine the HTTP method to use when following a redirect.
|
|
12
|
+
#
|
|
13
|
+
# The rules follow RFC 9110 and the WHATWG Fetch standard:
|
|
14
|
+
#
|
|
15
|
+
# - 301 and 302 change POST to GET; every other method is preserved.
|
|
16
|
+
# The QUERY specification states this POST exception does not apply
|
|
17
|
+
# to QUERY, so a QUERY is re-sent as a QUERY.
|
|
18
|
+
# - 303 preserves GET and HEAD; every other method becomes GET.
|
|
19
|
+
# - 300, 307, and 308 preserve the method.
|
|
20
|
+
#
|
|
21
|
+
# @param http_method [Symbol] the current request method
|
|
22
|
+
# @param status [Integer] the redirect status code
|
|
23
|
+
# @return [Symbol] the method for the redirected request
|
|
24
|
+
def redirect_method(http_method, status)
|
|
25
|
+
case status
|
|
26
|
+
when 301, 302
|
|
27
|
+
(http_method == :post) ? :get : http_method
|
|
28
|
+
when 303
|
|
29
|
+
%i[get head].include?(http_method) ? http_method : :get
|
|
30
|
+
else
|
|
31
|
+
http_method
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if following a redirect requires changing the request method.
|
|
36
|
+
#
|
|
37
|
+
# @param http_method [Symbol] the current request method
|
|
38
|
+
# @param status [Integer] the redirect status code
|
|
39
|
+
# @return [Boolean] true if the method must change to follow the redirect
|
|
40
|
+
def method_change_required?(http_method, status)
|
|
41
|
+
redirect_method(http_method, status) != http_method
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Normalize header names used to strip headers from redirected requests.
|
|
45
|
+
# Names are downcased so they match header names case insensitively.
|
|
46
|
+
#
|
|
47
|
+
# @param names [String, Symbol, Array<String, Symbol>, nil] header names
|
|
48
|
+
# @return [Array<String>] frozen lowercase header names
|
|
49
|
+
# @raise [ArgumentError] if a name is not a string or symbol, or is empty
|
|
50
|
+
def normalize_header_names(names)
|
|
51
|
+
Array(names).map do |name|
|
|
52
|
+
unless name.is_a?(String) || name.is_a?(Symbol)
|
|
53
|
+
raise ArgumentError.new("header names must be strings, got: #{name.inspect}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
name = name.to_s.downcase
|
|
57
|
+
raise ArgumentError.new("header names cannot be empty") if name.empty?
|
|
58
|
+
name.freeze
|
|
59
|
+
end.freeze
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
9
63
|
private
|
|
10
64
|
|
|
11
65
|
# Check if a redirect response should be followed.
|
|
@@ -21,9 +75,38 @@ module PatientHttp
|
|
|
21
75
|
location = response_data[:headers]["location"]
|
|
22
76
|
return false if location.nil? || location.empty?
|
|
23
77
|
|
|
78
|
+
if RedirectHelper.method_change_required?(task.request.http_method, status)
|
|
79
|
+
return false unless follow_method_changing_redirect?(task)
|
|
80
|
+
end
|
|
81
|
+
|
|
24
82
|
true
|
|
25
83
|
end
|
|
26
84
|
|
|
85
|
+
# Check if the request may change its method to follow a redirect.
|
|
86
|
+
# The request setting takes precedence over the configuration.
|
|
87
|
+
#
|
|
88
|
+
# @param task [RequestTask] the request task
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def follow_method_changing_redirect?(task)
|
|
91
|
+
value = task.request.follow_method_changing_redirects
|
|
92
|
+
value = @config.follow_method_changing_redirects if value.nil?
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Build the task for following a redirect, applying the configured
|
|
97
|
+
# header stripping rules.
|
|
98
|
+
#
|
|
99
|
+
# @param task [RequestTask] the request task
|
|
100
|
+
# @param response_data [Hash] the response data with status, headers, body
|
|
101
|
+
# @return [RequestTask] the redirect task
|
|
102
|
+
def build_redirect_task(task, response_data)
|
|
103
|
+
task.redirect_task(
|
|
104
|
+
location: response_data[:headers]["location"],
|
|
105
|
+
status: response_data[:status],
|
|
106
|
+
strip_headers: @config.redirect_strip_headers
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
27
110
|
# Check for either too-many-redirects or recursive redirect.
|
|
28
111
|
#
|
|
29
112
|
# @param task [RequestTask] the request task
|
data/lib/patient_http/request.rb
CHANGED
|
@@ -17,9 +17,12 @@ module PatientHttp
|
|
|
17
17
|
private_constant :UNDEFINED
|
|
18
18
|
|
|
19
19
|
# Valid HTTP methods
|
|
20
|
-
VALID_METHODS = %i[get post put patch delete].freeze
|
|
20
|
+
VALID_METHODS = %i[get head post put patch delete query].freeze
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# HTTP methods that must not carry a request body
|
|
23
|
+
BODYLESS_METHODS = %i[get head delete].freeze
|
|
24
|
+
|
|
25
|
+
# @return [Symbol] HTTP method (:get, :head, :post, :put, :patch, :delete, :query)
|
|
23
26
|
attr_reader :http_method
|
|
24
27
|
|
|
25
28
|
# @return [String] The request URL
|
|
@@ -34,6 +37,14 @@ module PatientHttp
|
|
|
34
37
|
# @return [Integer, nil] Maximum number of redirects to follow (nil uses config default, 0 disables)
|
|
35
38
|
attr_reader :max_redirects
|
|
36
39
|
|
|
40
|
+
# @return [Boolean, nil] Whether a redirect that requires changing the HTTP method
|
|
41
|
+
# (for example POST to GET on a 302) may be followed (nil uses config default)
|
|
42
|
+
attr_reader :follow_method_changing_redirects
|
|
43
|
+
|
|
44
|
+
# @return [Array<String>] Lowercase header names stripped from redirected requests,
|
|
45
|
+
# in addition to those configured on the {Configuration}
|
|
46
|
+
attr_reader :redirect_strip_headers
|
|
47
|
+
|
|
37
48
|
# @return [Hash{String, Symbol => SecretReference}] Query parameters whose values are
|
|
38
49
|
# secret references, kept out of the serialized URL and resolved at send time
|
|
39
50
|
attr_reader :secret_params
|
|
@@ -61,6 +72,8 @@ module PatientHttp
|
|
|
61
72
|
params: load_secret_params(hash["secret_params"]),
|
|
62
73
|
timeout: hash["timeout"],
|
|
63
74
|
max_redirects: hash["max_redirects"],
|
|
75
|
+
follow_method_changing_redirects: hash["follow_method_changing_redirects"],
|
|
76
|
+
redirect_strip_headers: hash["redirect_strip_headers"],
|
|
64
77
|
preprocessors: hash["preprocessors"],
|
|
65
78
|
processor: hash["processor"]
|
|
66
79
|
)
|
|
@@ -87,7 +100,7 @@ module PatientHttp
|
|
|
87
100
|
|
|
88
101
|
# Initializes a new Request.
|
|
89
102
|
#
|
|
90
|
-
# @param http_method [Symbol, String] HTTP method (:get, :post, :put, :patch, :delete).
|
|
103
|
+
# @param http_method [Symbol, String] HTTP method (:get, :head, :post, :put, :patch, :delete, :query).
|
|
91
104
|
# @param url [String, URI::Generic] The request URL.
|
|
92
105
|
# @param headers [Hash, HttpHeaders] Request headers.
|
|
93
106
|
# @param body [String, nil] Request body.
|
|
@@ -95,6 +108,12 @@ module PatientHttp
|
|
|
95
108
|
# @param params [Hash, nil] Query parameters to append to the URL.
|
|
96
109
|
# @param timeout [Numeric, nil] Overall timeout in seconds.
|
|
97
110
|
# @param max_redirects [Integer, nil] Maximum redirects to follow (nil uses config, 0 disables).
|
|
111
|
+
# @param follow_method_changing_redirects [Boolean, nil] Whether to follow a redirect that requires changing
|
|
112
|
+
# the HTTP method (nil uses config). When false, such a redirect response is returned as the
|
|
113
|
+
# result instead of being followed.
|
|
114
|
+
# @param redirect_strip_headers [String, Array<String>, nil] Header names (case insensitive)
|
|
115
|
+
# to strip from redirected requests, in addition to those configured on the
|
|
116
|
+
# {Configuration}.
|
|
98
117
|
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] Names of preprocessors
|
|
99
118
|
# registered on the configuration to apply to the request when it is sent.
|
|
100
119
|
# @param processor [String, Symbol, nil] Name of the processor that should execute the
|
|
@@ -108,6 +127,8 @@ module PatientHttp
|
|
|
108
127
|
params: nil,
|
|
109
128
|
timeout: nil,
|
|
110
129
|
max_redirects: nil,
|
|
130
|
+
follow_method_changing_redirects: nil,
|
|
131
|
+
redirect_strip_headers: nil,
|
|
111
132
|
preprocessors: nil,
|
|
112
133
|
processor: nil
|
|
113
134
|
)
|
|
@@ -125,6 +146,8 @@ module PatientHttp
|
|
|
125
146
|
@body = (body == "") ? nil : body
|
|
126
147
|
@timeout = timeout
|
|
127
148
|
@max_redirects = max_redirects
|
|
149
|
+
@follow_method_changing_redirects = normalized_follow_method_changing_redirects(follow_method_changing_redirects)
|
|
150
|
+
@redirect_strip_headers = RedirectHelper.normalize_header_names(redirect_strip_headers)
|
|
128
151
|
@preprocessors = normalized_preprocessors(preprocessors)
|
|
129
152
|
@processor = normalized_processor(processor)
|
|
130
153
|
|
|
@@ -167,6 +190,12 @@ module PatientHttp
|
|
|
167
190
|
hash["secret_params"] = @secret_params.transform_values(&:as_json)
|
|
168
191
|
end
|
|
169
192
|
|
|
193
|
+
unless @follow_method_changing_redirects.nil?
|
|
194
|
+
hash["follow_method_changing_redirects"] = @follow_method_changing_redirects
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
hash["redirect_strip_headers"] = @redirect_strip_headers if @redirect_strip_headers.any?
|
|
198
|
+
|
|
170
199
|
hash["preprocessors"] = @preprocessors if @preprocessors.any?
|
|
171
200
|
hash["processor"] = @processor if @processor
|
|
172
201
|
|
|
@@ -182,6 +211,14 @@ module PatientHttp
|
|
|
182
211
|
end
|
|
183
212
|
end
|
|
184
213
|
|
|
214
|
+
# Normalize the method-changing redirect flag to true, false, or nil.
|
|
215
|
+
def normalized_follow_method_changing_redirects(value)
|
|
216
|
+
return nil if value.nil?
|
|
217
|
+
return value if value == true || value == false
|
|
218
|
+
|
|
219
|
+
raise ArgumentError.new("follow_method_changing_redirects must be true, false, or nil, got: #{value.inspect}")
|
|
220
|
+
end
|
|
221
|
+
|
|
185
222
|
# Normalize the processor name to a frozen string or nil.
|
|
186
223
|
def normalized_processor(processor)
|
|
187
224
|
return nil if processor.nil?
|
|
@@ -238,7 +275,7 @@ module PatientHttp
|
|
|
238
275
|
raise ArgumentError.new("url must be a String or URI, got: #{@url.class}")
|
|
239
276
|
end
|
|
240
277
|
|
|
241
|
-
if
|
|
278
|
+
if BODYLESS_METHODS.include?(@http_method) && !@body.nil?
|
|
242
279
|
raise ArgumentError.new("body is not allowed for #{@http_method.upcase} requests")
|
|
243
280
|
end
|
|
244
281
|
|
|
@@ -16,8 +16,8 @@ module PatientHttp
|
|
|
16
16
|
# 1. Register a global request handler with {PatientHttp.register_handler}.
|
|
17
17
|
# 2. Include this module in a class.
|
|
18
18
|
# 3. Optionally configure defaults with {.request_template}.
|
|
19
|
-
# 4. Call `async_get`, `async_post`, `async_put`, `async_patch`,
|
|
20
|
-
# `async_request`.
|
|
19
|
+
# 4. Call `async_get`, `async_head`, `async_post`, `async_put`, `async_patch`,
|
|
20
|
+
# `async_delete`, `async_query`, or `async_request`.
|
|
21
21
|
#
|
|
22
22
|
# @example Register a handler
|
|
23
23
|
# PatientHttp.register_handler do |request:, callback:, callback_args: nil, raise_error_responses: nil|
|
|
@@ -62,6 +62,16 @@ module PatientHttp
|
|
|
62
62
|
async_request(:get, uri, callback: callback, **kwargs)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Enqueues an asynchronous HTTP HEAD request.
|
|
66
|
+
#
|
|
67
|
+
# @param uri [String] absolute URL or path (when using a request template)
|
|
68
|
+
# @param callback [Class, String] callback class to handle the response
|
|
69
|
+
# @param kwargs [Hash] forwarded to `async_request`
|
|
70
|
+
# @return [Object] return value from the registered request handler
|
|
71
|
+
def async_head(uri, callback:, **kwargs)
|
|
72
|
+
async_request(:head, uri, callback: callback, **kwargs)
|
|
73
|
+
end
|
|
74
|
+
|
|
65
75
|
# Enqueues an asynchronous HTTP POST request.
|
|
66
76
|
#
|
|
67
77
|
# @param uri [String] absolute URL or path (when using a request template)
|
|
@@ -101,6 +111,16 @@ module PatientHttp
|
|
|
101
111
|
def async_delete(uri, callback:, **kwargs)
|
|
102
112
|
async_request(:delete, uri, callback: callback, **kwargs)
|
|
103
113
|
end
|
|
114
|
+
|
|
115
|
+
# Enqueues an asynchronous HTTP QUERY request.
|
|
116
|
+
#
|
|
117
|
+
# @param uri [String] absolute URL or path (when using a request template)
|
|
118
|
+
# @param callback [Class, String] callback class to handle the response
|
|
119
|
+
# @param kwargs [Hash] forwarded to `async_request`
|
|
120
|
+
# @return [Object] return value from the registered request handler
|
|
121
|
+
def async_query(uri, callback:, **kwargs)
|
|
122
|
+
async_request(:query, uri, callback: callback, **kwargs)
|
|
123
|
+
end
|
|
104
124
|
end
|
|
105
125
|
|
|
106
126
|
module ClassMethods
|
|
@@ -134,7 +154,7 @@ module PatientHttp
|
|
|
134
154
|
# When a request template is configured, the request is built from the template. Otherwise,
|
|
135
155
|
# it is built directly from the provided arguments.
|
|
136
156
|
#
|
|
137
|
-
# @param method [Symbol] HTTP method (`:get`, `:post`, `:put`, `:patch`, `:delete`)
|
|
157
|
+
# @param method [Symbol] HTTP method (`:get`, `:head`, `:post`, `:put`, `:patch`, `:delete`, `:query`)
|
|
138
158
|
# @param url [String] absolute URL or path (when using a request template)
|
|
139
159
|
# @param callback [Class, String] callback class to handle the response
|
|
140
160
|
# @param headers [Hash, nil] request headers
|
|
@@ -145,6 +165,10 @@ module PatientHttp
|
|
|
145
165
|
# @param raise_error_responses [Boolean, nil] when true, non-success responses are
|
|
146
166
|
# reported as errors
|
|
147
167
|
# @param callback_args [Hash, nil] JSON-compatible callback arguments
|
|
168
|
+
# @param follow_method_changing_redirects [Boolean, nil] whether to follow a redirect that changes the
|
|
169
|
+
# HTTP method (nil uses the configuration default)
|
|
170
|
+
# @param redirect_strip_headers [String, Array<String>, nil] header names (case insensitive)
|
|
171
|
+
# to strip from redirected requests, in addition to the configured names
|
|
148
172
|
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
|
|
149
173
|
# registered on the configuration to apply to the request when it is sent
|
|
150
174
|
# @param processor [String, Symbol, nil] name of the processor that should execute
|
|
@@ -161,11 +185,23 @@ module PatientHttp
|
|
|
161
185
|
timeout: nil,
|
|
162
186
|
raise_error_responses: nil,
|
|
163
187
|
callback_args: nil,
|
|
188
|
+
follow_method_changing_redirects: nil,
|
|
189
|
+
redirect_strip_headers: nil,
|
|
164
190
|
preprocessors: nil,
|
|
165
191
|
processor: nil
|
|
166
192
|
)
|
|
167
193
|
template = async_request_template
|
|
168
|
-
kwargs = {
|
|
194
|
+
kwargs = {
|
|
195
|
+
body: body,
|
|
196
|
+
json: json,
|
|
197
|
+
headers: headers,
|
|
198
|
+
params: params,
|
|
199
|
+
timeout: timeout,
|
|
200
|
+
follow_method_changing_redirects: follow_method_changing_redirects,
|
|
201
|
+
redirect_strip_headers: redirect_strip_headers,
|
|
202
|
+
preprocessors: preprocessors,
|
|
203
|
+
processor: processor
|
|
204
|
+
}
|
|
169
205
|
request = if template
|
|
170
206
|
template.request(method, url, **kwargs)
|
|
171
207
|
else
|
|
@@ -198,7 +234,7 @@ module PatientHttp
|
|
|
198
234
|
#
|
|
199
235
|
# This delegates to {.ClassMethods#async_request} on the including class.
|
|
200
236
|
#
|
|
201
|
-
# @param method [Symbol] HTTP method (`:get`, `:post`, `:put`, `:patch`, `:delete`)
|
|
237
|
+
# @param method [Symbol] HTTP method (`:get`, `:head`, `:post`, `:put`, `:patch`, `:delete`, `:query`)
|
|
202
238
|
# @param url [String] absolute URL or path (when using a request template)
|
|
203
239
|
# @param callback [Class, String] callback class to handle the response
|
|
204
240
|
# @param headers [Hash, nil] request headers
|
|
@@ -209,6 +245,10 @@ module PatientHttp
|
|
|
209
245
|
# @param raise_error_responses [Boolean, nil] when true, non-success responses are
|
|
210
246
|
# reported as errors
|
|
211
247
|
# @param callback_args [Hash, nil] JSON-compatible callback arguments
|
|
248
|
+
# @param follow_method_changing_redirects [Boolean, nil] whether to follow a redirect that changes the
|
|
249
|
+
# HTTP method (nil uses the configuration default)
|
|
250
|
+
# @param redirect_strip_headers [String, Array<String>, nil] header names (case insensitive)
|
|
251
|
+
# to strip from redirected requests, in addition to the configured names
|
|
212
252
|
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
|
|
213
253
|
# registered on the configuration to apply to the request when it is sent
|
|
214
254
|
# @param processor [String, Symbol, nil] name of the processor that should execute
|
|
@@ -225,6 +265,8 @@ module PatientHttp
|
|
|
225
265
|
timeout: nil,
|
|
226
266
|
raise_error_responses: nil,
|
|
227
267
|
callback_args: nil,
|
|
268
|
+
follow_method_changing_redirects: nil,
|
|
269
|
+
redirect_strip_headers: nil,
|
|
228
270
|
preprocessors: nil,
|
|
229
271
|
processor: nil
|
|
230
272
|
)
|
|
@@ -239,6 +281,8 @@ module PatientHttp
|
|
|
239
281
|
timeout: timeout,
|
|
240
282
|
raise_error_responses: raise_error_responses,
|
|
241
283
|
callback_args: callback_args,
|
|
284
|
+
follow_method_changing_redirects: follow_method_changing_redirects,
|
|
285
|
+
redirect_strip_headers: redirect_strip_headers,
|
|
242
286
|
preprocessors: preprocessors,
|
|
243
287
|
processor: processor
|
|
244
288
|
)
|
|
@@ -10,6 +10,9 @@ module PatientHttp
|
|
|
10
10
|
# Headers that are sensitive to origin and should be stripped on cross-origin redirects
|
|
11
11
|
SENSITIVE_HEADERS = %w[authorization cookie].freeze
|
|
12
12
|
|
|
13
|
+
# Headers that describe a request body. They are removed when a redirect drops the body.
|
|
14
|
+
BODY_HEADERS = %w[content-type content-length content-encoding content-language content-location].freeze
|
|
15
|
+
|
|
13
16
|
# @return [String] Unique UUID for tracking the task
|
|
14
17
|
attr_reader :id
|
|
15
18
|
|
|
@@ -204,20 +207,24 @@ module PatientHttp
|
|
|
204
207
|
|
|
205
208
|
# Create a new RequestTask for following a redirect.
|
|
206
209
|
#
|
|
210
|
+
# The HTTP method follows RFC 9110: 301 and 302 change POST to GET, 303
|
|
211
|
+
# changes everything except GET and HEAD to GET, and 300, 307, and 308
|
|
212
|
+
# preserve the method. The body and the headers that describe it are
|
|
213
|
+
# dropped whenever the method changes.
|
|
214
|
+
#
|
|
215
|
+
# Headers named in the request's own redirect_strip_headers or in the given
|
|
216
|
+
# list are removed from the redirected request. Authorization and Cookie
|
|
217
|
+
# headers and preprocessors are removed on cross-origin redirects.
|
|
218
|
+
#
|
|
207
219
|
# @param location [String] The redirect URL from the Location header
|
|
208
220
|
# @param status [Integer] The HTTP status code of the redirect response
|
|
221
|
+
# @param strip_headers [Array<String>] Additional header names to strip,
|
|
222
|
+
# typically from the {Configuration}
|
|
209
223
|
# @return [RequestTask] A new task configured for the redirect
|
|
210
|
-
def redirect_task(location:, status:)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if [301, 302, 303].include?(status)
|
|
215
|
-
redirect_method = :get
|
|
216
|
-
redirect_body = nil
|
|
217
|
-
else
|
|
218
|
-
redirect_method = request.http_method
|
|
219
|
-
redirect_body = request.body
|
|
220
|
-
end
|
|
224
|
+
def redirect_task(location:, status:, strip_headers: [])
|
|
225
|
+
redirect_method = RedirectHelper.redirect_method(request.http_method, status)
|
|
226
|
+
method_changed = (redirect_method != request.http_method)
|
|
227
|
+
redirect_body = method_changed ? nil : request.body
|
|
221
228
|
|
|
222
229
|
# Resolve the redirect URL (handle relative URLs)
|
|
223
230
|
redirect_url = resolve_redirect_url(location)
|
|
@@ -226,8 +233,12 @@ module PatientHttp
|
|
|
226
233
|
# prevent credential leakage
|
|
227
234
|
cross_origin = cross_origin?(request.url, redirect_url)
|
|
228
235
|
redirect_headers = cross_origin ? request.headers.except(*SENSITIVE_HEADERS) : request.headers
|
|
236
|
+
redirect_headers = redirect_headers.except(*BODY_HEADERS) if method_changed
|
|
229
237
|
redirect_preprocessors = cross_origin ? [] : request.preprocessors
|
|
230
238
|
|
|
239
|
+
strip_names = request.redirect_strip_headers + Array(strip_headers)
|
|
240
|
+
redirect_headers = redirect_headers.except(*strip_names) if strip_names.any?
|
|
241
|
+
|
|
231
242
|
# Create a new request for the redirect
|
|
232
243
|
redirect_request = Request.new(
|
|
233
244
|
redirect_method,
|
|
@@ -236,6 +247,8 @@ module PatientHttp
|
|
|
236
247
|
body: redirect_body,
|
|
237
248
|
timeout: request.timeout,
|
|
238
249
|
max_redirects: request.max_redirects,
|
|
250
|
+
follow_method_changing_redirects: request.follow_method_changing_redirects,
|
|
251
|
+
redirect_strip_headers: request.redirect_strip_headers,
|
|
239
252
|
preprocessors: redirect_preprocessors,
|
|
240
253
|
processor: request.processor
|
|
241
254
|
)
|
|
@@ -46,18 +46,35 @@ module PatientHttp
|
|
|
46
46
|
|
|
47
47
|
# Build an async HTTP request. Returns a Request object.
|
|
48
48
|
#
|
|
49
|
-
# @param method [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
|
|
49
|
+
# @param method [Symbol] HTTP method (:get, :head, :post, :put, :patch, :delete, :query)
|
|
50
50
|
# @param uri [String, URI::HTTP] URI path to request (joined with base_url if relative)
|
|
51
51
|
# @param body [String, nil] request body
|
|
52
52
|
# @param json [Object, nil] JSON object to serialize (cannot use with body)
|
|
53
53
|
# @param headers [Hash] additional headers to merge with client headers
|
|
54
54
|
# @param params [Hash, nil] query parameters to add to URL
|
|
55
|
+
# @param timeout [Numeric, nil] request timeout in seconds (overrides the template default)
|
|
56
|
+
# @param follow_method_changing_redirects [Boolean, nil] whether to follow a redirect that changes the
|
|
57
|
+
# HTTP method (nil uses the configuration default)
|
|
58
|
+
# @param redirect_strip_headers [String, Array<String>, nil] header names (case insensitive)
|
|
59
|
+
# to strip from redirected requests, in addition to the configured names
|
|
55
60
|
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] preprocessors to apply
|
|
56
61
|
# to the request (overrides the template default)
|
|
57
62
|
# @param processor [String, Symbol, nil] processor name for the request (overrides the
|
|
58
63
|
# template default)
|
|
59
64
|
# @return [Request] request object
|
|
60
|
-
def request(
|
|
65
|
+
def request(
|
|
66
|
+
method,
|
|
67
|
+
uri,
|
|
68
|
+
body: nil,
|
|
69
|
+
json: nil,
|
|
70
|
+
headers: nil,
|
|
71
|
+
params: nil,
|
|
72
|
+
timeout: nil,
|
|
73
|
+
follow_method_changing_redirects: nil,
|
|
74
|
+
redirect_strip_headers: nil,
|
|
75
|
+
preprocessors: nil,
|
|
76
|
+
processor: nil
|
|
77
|
+
)
|
|
61
78
|
full_uri = @base_url ? URI.join(@base_url, uri.to_s) : URI(uri)
|
|
62
79
|
|
|
63
80
|
merged_headers = headers&.any? ? @headers.merge(headers) : @headers
|
|
@@ -72,6 +89,8 @@ module PatientHttp
|
|
|
72
89
|
json: json,
|
|
73
90
|
params: merged_params,
|
|
74
91
|
timeout: timeout || @timeout,
|
|
92
|
+
follow_method_changing_redirects: follow_method_changing_redirects,
|
|
93
|
+
redirect_strip_headers: redirect_strip_headers,
|
|
75
94
|
preprocessors: preprocessors || @preprocessors,
|
|
76
95
|
processor: processor || @processor
|
|
77
96
|
)
|
|
@@ -86,6 +105,15 @@ module PatientHttp
|
|
|
86
105
|
request(:get, uri, **kwargs)
|
|
87
106
|
end
|
|
88
107
|
|
|
108
|
+
# Convenience method for HEAD requests.
|
|
109
|
+
#
|
|
110
|
+
# @param uri [String, URI::HTTP] URI path to request
|
|
111
|
+
# @param kwargs [Hash] additional options (see #request)
|
|
112
|
+
# @return [Request] request object
|
|
113
|
+
def head(uri, **kwargs)
|
|
114
|
+
request(:head, uri, **kwargs)
|
|
115
|
+
end
|
|
116
|
+
|
|
89
117
|
# Convenience method for POST requests.
|
|
90
118
|
#
|
|
91
119
|
# @param uri [String, URI::HTTP] URI path to request
|
|
@@ -121,5 +149,14 @@ module PatientHttp
|
|
|
121
149
|
def delete(uri, **kwargs)
|
|
122
150
|
request(:delete, uri, **kwargs)
|
|
123
151
|
end
|
|
152
|
+
|
|
153
|
+
# Convenience method for QUERY requests.
|
|
154
|
+
#
|
|
155
|
+
# @param uri [String, URI::HTTP] URI path to request
|
|
156
|
+
# @param kwargs [Hash] additional options (see #request)
|
|
157
|
+
# @return [Request] request object
|
|
158
|
+
def query(uri, **kwargs)
|
|
159
|
+
request(:query, uri, **kwargs)
|
|
160
|
+
end
|
|
124
161
|
end
|
|
125
162
|
end
|
|
@@ -113,15 +113,21 @@ module PatientHttp
|
|
|
113
113
|
# response is compressed, the size check here applies to the compressed
|
|
114
114
|
# bytes and {#decode_body} applies the same limit to the inflated bytes.
|
|
115
115
|
#
|
|
116
|
+
# The Content-Length header is checked against the size limit before the
|
|
117
|
+
# read starts. A response to a HEAD request has an empty body but reports
|
|
118
|
+
# the Content-Length of the resource, so the header check is skipped when
|
|
119
|
+
# the body reports itself as empty.
|
|
120
|
+
#
|
|
116
121
|
# @param async_response [Async::HTTP::Protocol::Response] the async HTTP response
|
|
117
122
|
# @param headers_hash [Hash] the response headers
|
|
118
123
|
# @return [Array<String>, nil] the raw body chunks or nil if no body present
|
|
119
124
|
# @raise [ResponseTooLargeError] if the body exceeds max_response_size
|
|
120
125
|
# @raise [ReadAbortedError] if the processor stopped past its shutdown deadline mid-read
|
|
121
126
|
def read_raw_body(async_response, headers_hash)
|
|
122
|
-
|
|
127
|
+
body = async_response.body
|
|
128
|
+
return nil unless body
|
|
123
129
|
|
|
124
|
-
validate_content_length(headers_hash)
|
|
130
|
+
validate_content_length(headers_hash) unless body.empty?
|
|
125
131
|
read_body_chunks(async_response)
|
|
126
132
|
end
|
|
127
133
|
|
|
@@ -63,7 +63,7 @@ module PatientHttp
|
|
|
63
63
|
# flattened to a single joined string value.
|
|
64
64
|
headers_hash = async_response.headers.to_h.transform_values(&:to_s)
|
|
65
65
|
|
|
66
|
-
chunks =
|
|
66
|
+
chunks = @response_reader.read_raw_body(async_response, headers_hash)
|
|
67
67
|
body_content = @response_reader.decode_body(chunks, headers_hash)
|
|
68
68
|
|
|
69
69
|
{
|
|
@@ -81,8 +81,7 @@ module PatientHttp
|
|
|
81
81
|
redirect_error = check_redirect_error(@task, response_data)
|
|
82
82
|
break if redirect_error
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
@task = @task.redirect_task(location: location, status: response_data[:status])
|
|
84
|
+
@task = build_redirect_task(@task, response_data)
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
if redirect_error
|
|
@@ -179,47 +178,6 @@ module PatientHttp
|
|
|
179
178
|
)
|
|
180
179
|
end
|
|
181
180
|
|
|
182
|
-
# Read the raw response body chunks with size validation. The chunks are
|
|
183
|
-
# the wire bytes; ResponseReader#decode_body inflates and applies the
|
|
184
|
-
# charset, enforcing the same limit on the inflated bytes.
|
|
185
|
-
#
|
|
186
|
-
# @param async_response [Async::HTTP::Protocol::Response] the async HTTP response
|
|
187
|
-
# @param headers_hash [Hash] the response headers
|
|
188
|
-
# @return [Array<String>, nil] the raw body chunks or nil if no body present
|
|
189
|
-
def read_response_body(async_response, headers_hash)
|
|
190
|
-
return nil unless async_response.body
|
|
191
|
-
|
|
192
|
-
content_length = headers_hash["content-length"]&.to_i
|
|
193
|
-
if content_length && content_length > @config.max_response_size
|
|
194
|
-
raise ResponseTooLargeError.new(
|
|
195
|
-
"Response body size (#{content_length} bytes) exceeds maximum allowed size (#{@config.max_response_size} bytes)"
|
|
196
|
-
)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
chunks = []
|
|
200
|
-
total_size = 0
|
|
201
|
-
finished = false
|
|
202
|
-
|
|
203
|
-
begin
|
|
204
|
-
async_response.body.each do |chunk|
|
|
205
|
-
total_size += chunk.bytesize
|
|
206
|
-
if total_size > @config.max_response_size
|
|
207
|
-
raise ResponseTooLargeError.new(
|
|
208
|
-
"Response body size exceeded maximum allowed size (#{@config.max_response_size} bytes)"
|
|
209
|
-
)
|
|
210
|
-
end
|
|
211
|
-
chunks << chunk
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
finished = true
|
|
215
|
-
ensure
|
|
216
|
-
# Close the body if the read was interrupted so the connection is released
|
|
217
|
-
async_response.body.close unless finished
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
chunks
|
|
221
|
-
end
|
|
222
|
-
|
|
223
181
|
# Invoke callback synchronously.
|
|
224
182
|
#
|
|
225
183
|
# @param result [Response, Error] the result to pass to callback
|
data/lib/patient_http.rb
CHANGED
|
@@ -36,8 +36,9 @@ module PatientHttp
|
|
|
36
36
|
# lands in the job system's retry mechanism instead of being dropped.
|
|
37
37
|
class UnknownProcessorError < StandardError; end
|
|
38
38
|
|
|
39
|
-
# HTTP redirect status codes that
|
|
40
|
-
|
|
39
|
+
# HTTP redirect status codes that are followed when a Location header is present.
|
|
40
|
+
# A 300 response is followed only when the server names a preferred choice in Location.
|
|
41
|
+
FOLLOWABLE_REDIRECT_STATUSES = [300, 301, 302, 303, 307, 308].freeze
|
|
41
42
|
|
|
42
43
|
VERSION = File.read(File.join(__dir__, "../VERSION")).strip
|
|
43
44
|
|
|
@@ -275,6 +276,16 @@ module PatientHttp
|
|
|
275
276
|
request(:get, uri, callback: callback, **kwargs)
|
|
276
277
|
end
|
|
277
278
|
|
|
279
|
+
# Enqueues an HTTP HEAD request.
|
|
280
|
+
#
|
|
281
|
+
# @param uri [String] absolute URL
|
|
282
|
+
# @param callback [Class, String] callback class to handle the response
|
|
283
|
+
# @param kwargs [Hash] forwarded to `request`
|
|
284
|
+
# @return [Object] return value from the registered request handler
|
|
285
|
+
def head(uri, callback:, **kwargs)
|
|
286
|
+
request(:head, uri, callback: callback, **kwargs)
|
|
287
|
+
end
|
|
288
|
+
|
|
278
289
|
# Enqueues an HTTP POST request.
|
|
279
290
|
#
|
|
280
291
|
# @param uri [String] absolute URL
|
|
@@ -315,9 +326,19 @@ module PatientHttp
|
|
|
315
326
|
request(:delete, uri, callback: callback, **kwargs)
|
|
316
327
|
end
|
|
317
328
|
|
|
329
|
+
# Enqueues an HTTP QUERY request.
|
|
330
|
+
#
|
|
331
|
+
# @param uri [String] absolute URL
|
|
332
|
+
# @param callback [Class, String] callback class to handle the response
|
|
333
|
+
# @param kwargs [Hash] forwarded to `request`
|
|
334
|
+
# @return [Object] return value from the registered request handler
|
|
335
|
+
def query(uri, callback:, **kwargs)
|
|
336
|
+
request(:query, uri, callback: callback, **kwargs)
|
|
337
|
+
end
|
|
338
|
+
|
|
318
339
|
# Builds and dispatches an HTTP request.
|
|
319
340
|
#
|
|
320
|
-
# @param method [Symbol] HTTP method (`:get`, `:post`, `:put`, `:patch`, `:delete`)
|
|
341
|
+
# @param method [Symbol] HTTP method (`:get`, `:head`, `:post`, `:put`, `:patch`, `:delete`, `:query`)
|
|
321
342
|
# @param url [String] absolute URL
|
|
322
343
|
# @param callback [Class, String] callback class to handle the response
|
|
323
344
|
# @param headers [Hash, nil] request headers
|
|
@@ -328,6 +349,12 @@ module PatientHttp
|
|
|
328
349
|
# @param raise_error_responses [Boolean, nil] when true, non-success responses are
|
|
329
350
|
# reported as errors
|
|
330
351
|
# @param callback_args [Hash, nil] JSON-compatible callback arguments
|
|
352
|
+
# @param max_redirects [Integer, nil] maximum redirects to follow (nil uses the configuration
|
|
353
|
+
# default, 0 disables redirects)
|
|
354
|
+
# @param follow_method_changing_redirects [Boolean, nil] whether to follow a redirect that changes the
|
|
355
|
+
# HTTP method (nil uses the configuration default)
|
|
356
|
+
# @param redirect_strip_headers [String, Array<String>, nil] header names (case insensitive)
|
|
357
|
+
# to strip from redirected requests, in addition to the configured names
|
|
331
358
|
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
|
|
332
359
|
# registered on the configuration to apply to the request when it is sent
|
|
333
360
|
# @param processor [String, Symbol, nil] name of the processor that should execute
|
|
@@ -344,6 +371,9 @@ module PatientHttp
|
|
|
344
371
|
timeout: nil,
|
|
345
372
|
raise_error_responses: nil,
|
|
346
373
|
callback_args: nil,
|
|
374
|
+
max_redirects: nil,
|
|
375
|
+
follow_method_changing_redirects: nil,
|
|
376
|
+
redirect_strip_headers: nil,
|
|
347
377
|
preprocessors: nil,
|
|
348
378
|
processor: nil
|
|
349
379
|
)
|
|
@@ -355,6 +385,9 @@ module PatientHttp
|
|
|
355
385
|
headers: headers,
|
|
356
386
|
params: params,
|
|
357
387
|
timeout: timeout,
|
|
388
|
+
max_redirects: max_redirects,
|
|
389
|
+
follow_method_changing_redirects: follow_method_changing_redirects,
|
|
390
|
+
redirect_strip_headers: redirect_strip_headers,
|
|
358
391
|
preprocessors: preprocessors,
|
|
359
392
|
processor: processor
|
|
360
393
|
)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: patient_http
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
@@ -146,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
146
146
|
- !ruby/object:Gem::Version
|
|
147
147
|
version: '0'
|
|
148
148
|
requirements: []
|
|
149
|
-
rubygems_version:
|
|
149
|
+
rubygems_version: 4.0.3
|
|
150
150
|
specification_version: 4
|
|
151
151
|
summary: Generic async HTTP connection pool for Ruby applications using Fiber-based
|
|
152
152
|
concurrency
|