patient_http 1.6.0 → 1.7.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/ARCHITECTURE.md +8 -7
- data/CHANGELOG.md +39 -0
- data/README.md +540 -514
- data/VERSION +1 -1
- data/lib/patient_http/callback_args.rb +53 -48
- data/lib/patient_http/callback_validator.rb +11 -7
- data/lib/patient_http/class_helper.rb +6 -7
- data/lib/patient_http/client.rb +28 -22
- data/lib/patient_http/client_pool.rb +134 -39
- data/lib/patient_http/completion_executor.rb +20 -20
- data/lib/patient_http/configuration.rb +367 -119
- data/lib/patient_http/connection_endpoint.rb +150 -0
- data/lib/patient_http/encryptor.rb +28 -18
- data/lib/patient_http/error.rb +24 -18
- data/lib/patient_http/external_storage.rb +42 -38
- data/lib/patient_http/http_error.rb +30 -26
- data/lib/patient_http/http_headers.rb +57 -26
- data/lib/patient_http/immediate_retries.rb +98 -0
- data/lib/patient_http/inline_task_handler.rb +15 -10
- data/lib/patient_http/lifecycle_manager.rb +39 -40
- data/lib/patient_http/outgoing_request.rb +25 -23
- data/lib/patient_http/payload.rb +28 -26
- data/lib/patient_http/payload_store/active_record_store.rb +31 -34
- data/lib/patient_http/payload_store/base.rb +42 -46
- data/lib/patient_http/payload_store/file_store.rb +22 -26
- data/lib/patient_http/payload_store/redis_store.rb +28 -34
- data/lib/patient_http/payload_store/s3_store.rb +25 -28
- data/lib/patient_http/payload_store.rb +2 -0
- data/lib/patient_http/processor.rb +111 -79
- data/lib/patient_http/processor_observer.rb +65 -59
- data/lib/patient_http/rails/engine.rb +13 -8
- data/lib/patient_http/redirect_error.rb +50 -41
- data/lib/patient_http/redirect_helper.rb +38 -38
- data/lib/patient_http/request.rb +70 -46
- data/lib/patient_http/request_error.rb +47 -42
- data/lib/patient_http/request_helper.rb +142 -119
- data/lib/patient_http/request_preparer.rb +13 -10
- data/lib/patient_http/request_task.rb +113 -84
- data/lib/patient_http/request_template.rb +87 -64
- data/lib/patient_http/response.rb +58 -52
- data/lib/patient_http/response_reader.rb +66 -65
- data/lib/patient_http/secret_manager.rb +34 -30
- data/lib/patient_http/secret_reference.rb +33 -26
- data/lib/patient_http/synchronous_executor.rb +67 -95
- data/lib/patient_http/task_handler.rb +23 -19
- data/lib/patient_http/time_helper.rb +8 -8
- data/lib/patient_http.rb +311 -186
- data/patient_http.gemspec +3 -2
- metadata +20 -4
|
@@ -1,43 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# The HTTP response to an async request.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# A response holds the status, headers, and body, and details about the
|
|
7
|
+
# request that returned it. Responses can be serialized to JSON, so they can
|
|
8
|
+
# go through a job queue.
|
|
8
9
|
class Response
|
|
9
10
|
UNDEFINED = Object.new.freeze
|
|
10
11
|
private_constant :UNDEFINED
|
|
11
12
|
|
|
12
|
-
# @return [Integer] HTTP status code
|
|
13
|
+
# @return [Integer] The HTTP status code.
|
|
13
14
|
attr_reader :status
|
|
14
15
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
16
|
+
# The response headers. A header that occurs more than one time in the
|
|
17
|
+
# response, such as `set-cookie`, becomes one string with the values joined.
|
|
17
18
|
#
|
|
18
|
-
# @return [HttpHeaders] response headers
|
|
19
|
+
# @return [HttpHeaders] The response headers.
|
|
19
20
|
attr_reader :headers
|
|
20
21
|
|
|
21
|
-
# @return [Float] request duration in seconds
|
|
22
|
+
# @return [Float] The request duration in seconds.
|
|
22
23
|
attr_reader :duration
|
|
23
24
|
|
|
24
|
-
# @return [String] request ID
|
|
25
|
+
# @return [String] The request ID.
|
|
25
26
|
attr_reader :request_id
|
|
26
27
|
|
|
27
|
-
# @return [String] request URL
|
|
28
|
+
# @return [String] The request URL.
|
|
28
29
|
attr_reader :url
|
|
29
30
|
|
|
30
|
-
# @return [Symbol] HTTP method
|
|
31
|
+
# @return [Symbol] The HTTP method.
|
|
31
32
|
attr_reader :http_method
|
|
32
33
|
|
|
33
|
-
# @return [Array<String>] URLs
|
|
34
|
+
# @return [Array<String>] The URLs of the redirects that were followed, in
|
|
35
|
+
# order. Empty if no redirects were followed.
|
|
34
36
|
attr_reader :redirects
|
|
35
37
|
|
|
36
38
|
class << self
|
|
37
|
-
#
|
|
39
|
+
# Creates a response from its serialized form.
|
|
38
40
|
#
|
|
39
|
-
# @param hash [Hash] hash
|
|
40
|
-
# @return [Response]
|
|
41
|
+
# @param hash [Hash] The hash from {#as_json}.
|
|
42
|
+
# @return [Response] The response.
|
|
41
43
|
def load(hash)
|
|
42
44
|
new(
|
|
43
45
|
status: hash["status"],
|
|
@@ -53,17 +55,18 @@ module PatientHttp
|
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
#
|
|
58
|
+
# Creates a response.
|
|
57
59
|
#
|
|
58
|
-
# @param status [Integer] HTTP status code
|
|
59
|
-
# @param headers [Hash, HttpHeaders] response headers
|
|
60
|
-
# @param body [String, nil] response body
|
|
61
|
-
# @param duration [Float] request duration in seconds
|
|
62
|
-
# @param request_id [String]
|
|
63
|
-
# @param url [String]
|
|
64
|
-
# @param http_method [Symbol]
|
|
65
|
-
# @param callback_args [Hash, nil] callback arguments
|
|
66
|
-
# @param redirects [Array<String>, nil] URLs
|
|
60
|
+
# @param status [Integer] The HTTP status code.
|
|
61
|
+
# @param headers [Hash, HttpHeaders] The response headers.
|
|
62
|
+
# @param body [String, nil] The response body.
|
|
63
|
+
# @param duration [Float] The request duration in seconds.
|
|
64
|
+
# @param request_id [String] The request ID.
|
|
65
|
+
# @param url [String] The request URL.
|
|
66
|
+
# @param http_method [Symbol] The HTTP method.
|
|
67
|
+
# @param callback_args [Hash, nil] The callback arguments, with string keys.
|
|
68
|
+
# @param redirects [Array<String>, nil] The URLs of the redirects that were
|
|
69
|
+
# followed.
|
|
67
70
|
def initialize(status:, headers:, body:, duration:, request_id:, url:, http_method:, callback_args: nil, redirects: nil)
|
|
68
71
|
@status = status
|
|
69
72
|
@headers = HttpHeaders.new(headers)
|
|
@@ -80,76 +83,78 @@ module PatientHttp
|
|
|
80
83
|
@redirects = redirects || []
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
# Returns the callback arguments
|
|
86
|
+
# Returns the callback arguments that were passed with the request.
|
|
84
87
|
#
|
|
85
|
-
# @return [CallbackArgs]
|
|
88
|
+
# @return [CallbackArgs] The callback arguments.
|
|
86
89
|
def callback_args
|
|
87
90
|
@callback_args ||= CallbackArgs.load(@callback_args_data)
|
|
88
91
|
end
|
|
89
92
|
|
|
90
|
-
# Returns the response body
|
|
93
|
+
# Returns the response body. The body is decoded on first access.
|
|
91
94
|
#
|
|
92
|
-
# @return [String, nil] The
|
|
95
|
+
# @return [String, nil] The response body, or `nil` if the response has no
|
|
96
|
+
# body.
|
|
93
97
|
def body
|
|
94
98
|
@body = @payload&.value if @body.equal?(UNDEFINED)
|
|
95
99
|
@body
|
|
96
100
|
end
|
|
97
101
|
|
|
98
|
-
#
|
|
102
|
+
# Returns whether the status is 2xx.
|
|
99
103
|
#
|
|
100
|
-
# @return [Boolean]
|
|
104
|
+
# @return [Boolean] `true` if the request succeeded.
|
|
101
105
|
def success?
|
|
102
106
|
status >= 200 && status < 300
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
#
|
|
109
|
+
# Returns whether the status is 3xx.
|
|
106
110
|
#
|
|
107
|
-
# @return [Boolean]
|
|
111
|
+
# @return [Boolean] `true` if the response is a redirect.
|
|
108
112
|
def redirect?
|
|
109
113
|
status >= 300 && status < 400
|
|
110
114
|
end
|
|
111
115
|
|
|
112
|
-
#
|
|
116
|
+
# Returns whether the status is 4xx.
|
|
113
117
|
#
|
|
114
|
-
# @return [Boolean]
|
|
118
|
+
# @return [Boolean] `true` if the response is a client error.
|
|
115
119
|
def client_error?
|
|
116
120
|
status >= 400 && status < 500
|
|
117
121
|
end
|
|
118
122
|
|
|
119
|
-
#
|
|
123
|
+
# Returns whether the status is 5xx.
|
|
120
124
|
#
|
|
121
|
-
# @return [Boolean]
|
|
125
|
+
# @return [Boolean] `true` if the response is a server error.
|
|
122
126
|
def server_error?
|
|
123
127
|
status >= 500 && status < 600
|
|
124
128
|
end
|
|
125
129
|
|
|
126
|
-
#
|
|
130
|
+
# Returns whether the status is 4xx or 5xx.
|
|
127
131
|
#
|
|
128
|
-
# @return [Boolean]
|
|
132
|
+
# @return [Boolean] `true` if the response is an error.
|
|
129
133
|
def error?
|
|
130
134
|
status >= 400 && status < 600
|
|
131
135
|
end
|
|
132
136
|
|
|
133
|
-
#
|
|
137
|
+
# Returns the value of the `content-type` header.
|
|
134
138
|
#
|
|
135
|
-
# @return [String, nil]
|
|
139
|
+
# @return [String, nil] The content type, or `nil` if the header isn't set.
|
|
136
140
|
def content_type
|
|
137
141
|
headers["content-type"]
|
|
138
142
|
end
|
|
139
143
|
|
|
140
|
-
#
|
|
144
|
+
# Returns whether the `content-type` header identifies a JSON body.
|
|
141
145
|
#
|
|
142
|
-
# @return [Boolean]
|
|
146
|
+
# @return [Boolean] `true` if the body is JSON.
|
|
143
147
|
def json?
|
|
144
148
|
type = content_type.to_s.downcase
|
|
145
149
|
type.match?(%r{\Aapplication/[^ ]*json\b}) || type == "text/json"
|
|
146
150
|
end
|
|
147
151
|
|
|
148
|
-
#
|
|
152
|
+
# Parses the response body as JSON.
|
|
149
153
|
#
|
|
150
|
-
# @return [Hash, Array] parsed
|
|
151
|
-
# @raise [RuntimeError]
|
|
152
|
-
#
|
|
154
|
+
# @return [Hash, Array] The parsed body.
|
|
155
|
+
# @raise [RuntimeError] If the `content-type` header doesn't identify a JSON
|
|
156
|
+
# body.
|
|
157
|
+
# @raise [JSON::ParserError] If the body isn't valid JSON.
|
|
153
158
|
def json
|
|
154
159
|
unless json?
|
|
155
160
|
raise "Response Content-Type is not application/json (got: #{content_type.inspect})"
|
|
@@ -158,9 +163,9 @@ module PatientHttp
|
|
|
158
163
|
JSON.parse(body)
|
|
159
164
|
end
|
|
160
165
|
|
|
161
|
-
#
|
|
166
|
+
# Returns the response as a JSON-compatible hash.
|
|
162
167
|
#
|
|
163
|
-
# @return [Hash]
|
|
168
|
+
# @return [Hash] The serialized response.
|
|
164
169
|
def as_json
|
|
165
170
|
{
|
|
166
171
|
"status" => status,
|
|
@@ -175,10 +180,11 @@ module PatientHttp
|
|
|
175
180
|
}
|
|
176
181
|
end
|
|
177
182
|
|
|
178
|
-
#
|
|
183
|
+
# Returns the response as a JSON string.
|
|
179
184
|
#
|
|
180
|
-
# @param options [Hash] options
|
|
181
|
-
#
|
|
185
|
+
# @param options [Hash, nil] The options for `JSON.generate`. This parameter
|
|
186
|
+
# makes the method compatible with Active Support.
|
|
187
|
+
# @return [String] The JSON string.
|
|
182
188
|
def to_json(options = nil)
|
|
183
189
|
JSON.generate(as_json, options)
|
|
184
190
|
end
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
module PatientHttp
|
|
4
4
|
# Reads and decodes HTTP response bodies.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# The reactor thread reads the raw body chunks, which can be compressed, and
|
|
7
|
+
# checks their size. Decoding is a separate step that runs on a completion
|
|
8
|
+
# worker thread, so it doesn't block the reactor. Decoding joins the chunks,
|
|
9
|
+
# decompresses them, and applies the charset.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
11
12
|
class ResponseReader
|
|
12
13
|
# Raised when a body read is aborted because the processor was stopped
|
|
13
14
|
# past its shutdown deadline. The shutdown sequence re-enqueues the task,
|
|
@@ -34,7 +35,7 @@ module PatientHttp
|
|
|
34
35
|
IDENTITY_ENCODING = "identity"
|
|
35
36
|
|
|
36
37
|
class << self
|
|
37
|
-
#
|
|
38
|
+
# Splits the encodings named in the content-encoding header into the ones
|
|
38
39
|
# that stay applied to the body and the ones that can be decoded.
|
|
39
40
|
#
|
|
40
41
|
# A body can carry more than one encoding. They are listed in the order
|
|
@@ -42,9 +43,9 @@ module PatientHttp
|
|
|
42
43
|
# stops at the first name it does not recognize. Everything before that
|
|
43
44
|
# point stays applied to the body.
|
|
44
45
|
#
|
|
45
|
-
# @param headers_hash [Hash]
|
|
46
|
-
# @return [Array(Array<String>, Array<String>)]
|
|
47
|
-
# applied and the encodings that can be decoded, both in applied order
|
|
46
|
+
# @param headers_hash [Hash] The response headers.
|
|
47
|
+
# @return [Array(Array<String>, Array<String>)] The encodings that remain
|
|
48
|
+
# applied and the encodings that can be decoded, both in applied order.
|
|
48
49
|
def split_encodings(headers_hash)
|
|
49
50
|
encodings = content_encodings(headers_hash)
|
|
50
51
|
boundary = encodings.rindex { |name| !decodable?(name) }
|
|
@@ -53,10 +54,10 @@ module PatientHttp
|
|
|
53
54
|
[encodings[0..boundary], encodings[(boundary + 1)..]]
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
#
|
|
57
|
+
# Parses the content-encoding header into encoding names.
|
|
57
58
|
#
|
|
58
|
-
# @param headers_hash [Hash]
|
|
59
|
-
# @return [Array<String>]
|
|
59
|
+
# @param headers_hash [Hash] The response headers.
|
|
60
|
+
# @return [Array<String>] The lowercased encoding names in applied order.
|
|
60
61
|
def content_encodings(headers_hash)
|
|
61
62
|
headers_hash["content-encoding"].to_s.split(",").filter_map do |name|
|
|
62
63
|
name = name.strip.downcase
|
|
@@ -64,19 +65,19 @@ module PatientHttp
|
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
# @param name [String]
|
|
68
|
-
# @return [Boolean] true if the reader can remove this encoding
|
|
68
|
+
# @param name [String] A lowercased content encoding name.
|
|
69
|
+
# @return [Boolean] `true` if the reader can remove this encoding.
|
|
69
70
|
def decodable?(name)
|
|
70
71
|
name == IDENTITY_ENCODING || INFLATE_WINDOW_BITS.key?(name)
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
#
|
|
74
|
+
# Restates the content-encoding header for a decoded body. The header is
|
|
74
75
|
# removed when nothing is left applied, and narrowed to the encodings the
|
|
75
76
|
# reader could not remove otherwise, so the header always describes the
|
|
76
77
|
# body delivered with it.
|
|
77
78
|
#
|
|
78
|
-
# @param headers_hash [Hash]
|
|
79
|
-
# @return [Hash]
|
|
79
|
+
# @param headers_hash [Hash] The response headers.
|
|
80
|
+
# @return [Hash] The headers with content-encoding updated or removed.
|
|
80
81
|
def rewrite_content_encoding(headers_hash)
|
|
81
82
|
return headers_hash unless headers_hash.key?("content-encoding")
|
|
82
83
|
|
|
@@ -90,21 +91,21 @@ module PatientHttp
|
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
#
|
|
94
|
+
# Creates the reader.
|
|
94
95
|
#
|
|
95
96
|
# Reading needs a processor so it can abort once the processor is past its
|
|
96
97
|
# shutdown deadline. Decoding needs only the configuration, so a caller
|
|
97
98
|
# that does its own reading can supply the configuration on its own.
|
|
98
99
|
#
|
|
99
|
-
# @param processor [Processor, nil]
|
|
100
|
-
# @param config [Configuration, nil]
|
|
101
|
-
# processor's configuration
|
|
100
|
+
# @param processor [Processor, nil] The processor object.
|
|
101
|
+
# @param config [Configuration, nil] The configuration; defaults to the
|
|
102
|
+
# processor's configuration.
|
|
102
103
|
def initialize(processor, config: nil)
|
|
103
104
|
@processor = processor
|
|
104
105
|
@config = config || processor.config
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
#
|
|
108
|
+
# Reads the raw response body chunks with size validation.
|
|
108
109
|
#
|
|
109
110
|
# Reads the async HTTP response body asynchronously to completion, which allows
|
|
110
111
|
# the connection to be reused. The async-http client handles connection pooling
|
|
@@ -118,11 +119,11 @@ module PatientHttp
|
|
|
118
119
|
# the Content-Length of the resource, so the header check is skipped when
|
|
119
120
|
# the body reports itself as empty.
|
|
120
121
|
#
|
|
121
|
-
# @param async_response [Async::HTTP::Protocol::Response]
|
|
122
|
-
# @param headers_hash [Hash]
|
|
123
|
-
# @return [Array<String>, nil]
|
|
124
|
-
# @raise [ResponseTooLargeError]
|
|
125
|
-
# @raise [ReadAbortedError]
|
|
122
|
+
# @param async_response [Async::HTTP::Protocol::Response] The async HTTP response.
|
|
123
|
+
# @param headers_hash [Hash] The response headers.
|
|
124
|
+
# @return [Array<String>, nil] The raw body chunks or nil if no body present.
|
|
125
|
+
# @raise [ResponseTooLargeError] If the body exceeds max_response_size.
|
|
126
|
+
# @raise [ReadAbortedError] If the processor stopped past its shutdown deadline mid-read.
|
|
126
127
|
def read_raw_body(async_response, headers_hash)
|
|
127
128
|
body = async_response.body
|
|
128
129
|
return nil unless body
|
|
@@ -131,7 +132,7 @@ module PatientHttp
|
|
|
131
132
|
read_body_chunks(async_response)
|
|
132
133
|
end
|
|
133
134
|
|
|
134
|
-
#
|
|
135
|
+
# Decodes raw body chunks into the final body string.
|
|
135
136
|
#
|
|
136
137
|
# Joins the chunks, inflates gzip/deflate content (enforcing
|
|
137
138
|
# max_response_size on the inflated bytes), and applies the charset from
|
|
@@ -144,10 +145,10 @@ module PatientHttp
|
|
|
144
145
|
# so the content-encoding header delivered with the response describes the
|
|
145
146
|
# body it carries.
|
|
146
147
|
#
|
|
147
|
-
# @param chunks [Array<String>, nil]
|
|
148
|
-
# @param headers_hash [Hash]
|
|
149
|
-
# @return [String, nil]
|
|
150
|
-
# @raise [ResponseTooLargeError]
|
|
148
|
+
# @param chunks [Array<String>, nil] The raw body chunks.
|
|
149
|
+
# @param headers_hash [Hash] The response headers.
|
|
150
|
+
# @return [String, nil] The decoded body or nil if there was no body.
|
|
151
|
+
# @raise [ResponseTooLargeError] If the inflated body exceeds max_response_size.
|
|
151
152
|
def decode_body(chunks, headers_hash)
|
|
152
153
|
return nil if chunks.nil?
|
|
153
154
|
|
|
@@ -165,13 +166,13 @@ module PatientHttp
|
|
|
165
166
|
|
|
166
167
|
private
|
|
167
168
|
|
|
168
|
-
#
|
|
169
|
+
# Removes the given encodings from the body, starting with the one applied
|
|
169
170
|
# last. Identity needs no work; every other name here inflates.
|
|
170
171
|
#
|
|
171
|
-
# @param chunks [Array<String>]
|
|
172
|
-
# @param encodings [Array<String>]
|
|
173
|
-
# @return [Array<String>]
|
|
174
|
-
# @raise [ResponseTooLargeError]
|
|
172
|
+
# @param chunks [Array<String>] The encoded body chunks.
|
|
173
|
+
# @param encodings [Array<String>] Decodable encoding names in applied order.
|
|
174
|
+
# @return [Array<String>] The decoded chunks.
|
|
175
|
+
# @raise [ResponseTooLargeError] If the inflated body exceeds max_response_size.
|
|
175
176
|
def inflate_encodings(chunks, encodings)
|
|
176
177
|
encodings.reverse_each do |name|
|
|
177
178
|
chunks = [inflate_encoding(chunks, name)] if INFLATE_WINDOW_BITS.key?(name)
|
|
@@ -180,15 +181,15 @@ module PatientHttp
|
|
|
180
181
|
chunks
|
|
181
182
|
end
|
|
182
183
|
|
|
183
|
-
#
|
|
184
|
+
# Inflates one encoding, trying each wire format the encoding can use. The
|
|
184
185
|
# chunks are all in memory, so a format that turns out to be wrong can be
|
|
185
186
|
# abandoned and the next one started from the beginning of the body.
|
|
186
187
|
#
|
|
187
|
-
# @param chunks [Array<String>]
|
|
188
|
-
# @param name [String]
|
|
189
|
-
# @return [String]
|
|
190
|
-
# @raise [Zlib::Error]
|
|
191
|
-
# @raise [ResponseTooLargeError]
|
|
188
|
+
# @param chunks [Array<String>] The encoded body chunks.
|
|
189
|
+
# @param name [String] A lowercased content encoding name.
|
|
190
|
+
# @return [String] The inflated body.
|
|
191
|
+
# @raise [Zlib::Error] If no format could inflate the body.
|
|
192
|
+
# @raise [ResponseTooLargeError] If the inflated body exceeds max_response_size.
|
|
192
193
|
def inflate_encoding(chunks, name)
|
|
193
194
|
formats = INFLATE_WINDOW_BITS.fetch(name)
|
|
194
195
|
last_index = formats.size - 1
|
|
@@ -200,11 +201,11 @@ module PatientHttp
|
|
|
200
201
|
end
|
|
201
202
|
end
|
|
202
203
|
|
|
203
|
-
#
|
|
204
|
+
# Reports an encoding that could not be removed. The body is still delivered
|
|
204
205
|
# with its content-encoding header, so the caller can decode it, but the
|
|
205
206
|
# server ignored the accept-encoding header and that is worth recording.
|
|
206
207
|
#
|
|
207
|
-
# @param remaining [Array<String>]
|
|
208
|
+
# @param remaining [Array<String>] The encodings left on the body.
|
|
208
209
|
# @return [void]
|
|
209
210
|
def warn_undecodable(remaining)
|
|
210
211
|
logger&.warn(
|
|
@@ -222,10 +223,10 @@ module PatientHttp
|
|
|
222
223
|
@config.logger
|
|
223
224
|
end
|
|
224
225
|
|
|
225
|
-
#
|
|
226
|
+
# Validates content-length header doesn't exceed max size.
|
|
226
227
|
#
|
|
227
|
-
# @param headers_hash [Hash]
|
|
228
|
-
# @raise [ResponseTooLargeError]
|
|
228
|
+
# @param headers_hash [Hash] The response headers.
|
|
229
|
+
# @raise [ResponseTooLargeError] If content-length exceeds max_response_size.
|
|
229
230
|
def validate_content_length(headers_hash)
|
|
230
231
|
content_length = headers_hash["content-length"]&.to_i
|
|
231
232
|
if content_length && content_length > max_response_size
|
|
@@ -235,12 +236,12 @@ module PatientHttp
|
|
|
235
236
|
end
|
|
236
237
|
end
|
|
237
238
|
|
|
238
|
-
#
|
|
239
|
+
# Reads body chunks while checking size.
|
|
239
240
|
#
|
|
240
|
-
# @param async_response [Async::HTTP::Protocol::Response]
|
|
241
|
-
# @return [Array<String>]
|
|
242
|
-
# @raise [ResponseTooLargeError]
|
|
243
|
-
# @raise [ReadAbortedError]
|
|
241
|
+
# @param async_response [Async::HTTP::Protocol::Response] The async HTTP response.
|
|
242
|
+
# @return [Array<String>] The raw body chunks.
|
|
243
|
+
# @raise [ResponseTooLargeError] If body size exceeds max_response_size during read.
|
|
244
|
+
# @raise [ReadAbortedError] If the processor stopped past its shutdown deadline mid-read.
|
|
244
245
|
def read_body_chunks(async_response)
|
|
245
246
|
chunks = []
|
|
246
247
|
total_size = 0
|
|
@@ -277,13 +278,13 @@ module PatientHttp
|
|
|
277
278
|
end
|
|
278
279
|
end
|
|
279
280
|
|
|
280
|
-
#
|
|
281
|
+
# Inflates compressed body chunks with streaming size enforcement, so a
|
|
281
282
|
# small compressed body cannot expand past max_response_size.
|
|
282
283
|
#
|
|
283
|
-
# @param chunks [Array<String>]
|
|
284
|
-
# @param window_bits [Integer] Zlib window bits for the content encoding
|
|
285
|
-
# @return [String]
|
|
286
|
-
# @raise [ResponseTooLargeError]
|
|
284
|
+
# @param chunks [Array<String>] The raw compressed chunks.
|
|
285
|
+
# @param window_bits [Integer] Zlib window bits for the content encoding.
|
|
286
|
+
# @return [String] The inflated body.
|
|
287
|
+
# @raise [ResponseTooLargeError] If the inflated size exceeds max_response_size.
|
|
287
288
|
def inflate_chunks(chunks, window_bits)
|
|
288
289
|
# A response can declare a content encoding and still carry no body.
|
|
289
290
|
# There is nothing to inflate, and finishing an empty stream would
|
|
@@ -320,10 +321,10 @@ module PatientHttp
|
|
|
320
321
|
end
|
|
321
322
|
end
|
|
322
323
|
|
|
323
|
-
#
|
|
324
|
+
# Extracts charset from Content-Type header.
|
|
324
325
|
#
|
|
325
|
-
# @param headers_hash [Hash]
|
|
326
|
-
# @return [String, nil]
|
|
326
|
+
# @param headers_hash [Hash] The response headers.
|
|
327
|
+
# @return [String, nil] The charset name or nil if not specified.
|
|
327
328
|
def extract_charset(headers_hash)
|
|
328
329
|
content_type = headers_hash["content-type"]
|
|
329
330
|
return nil unless content_type
|
|
@@ -335,14 +336,14 @@ module PatientHttp
|
|
|
335
336
|
charset.gsub(/\A["']|["']\z/, "")
|
|
336
337
|
end
|
|
337
338
|
|
|
338
|
-
#
|
|
339
|
+
# Applies charset encoding to response body.
|
|
339
340
|
#
|
|
340
341
|
# Sets the string encoding based on the charset specified in the Content-Type header.
|
|
341
342
|
# Falls back to ASCII-8BIT if charset is invalid or not recognized.
|
|
342
343
|
#
|
|
343
|
-
# @param body [String]
|
|
344
|
-
# @param headers_hash [Hash]
|
|
345
|
-
# @return [String]
|
|
344
|
+
# @param body [String] The response body.
|
|
345
|
+
# @param headers_hash [Hash] The response headers.
|
|
346
|
+
# @return [String] The body with proper encoding set.
|
|
346
347
|
def apply_charset_encoding(body, headers_hash)
|
|
347
348
|
return body unless body
|
|
348
349
|
|
|
@@ -1,41 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Replaces {SecretReference} values with the secret values when the processor
|
|
5
|
+
# sends a request.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# The manager holds the secrets registered in the {Configuration}.
|
|
8
8
|
#
|
|
9
9
|
# @see Configuration#secret_manager
|
|
10
10
|
class SecretManager
|
|
11
|
-
# Raised when a referenced secret
|
|
11
|
+
# Raised when a referenced secret isn't registered.
|
|
12
12
|
class SecretNotFoundError < StandardError; end
|
|
13
13
|
|
|
14
|
-
#
|
|
14
|
+
# Creates a secret manager.
|
|
15
15
|
#
|
|
16
|
-
# @param secrets [Hash{String => Object}]
|
|
17
|
-
#
|
|
18
|
-
# secret not found in the static registry
|
|
16
|
+
# @param secrets [Hash{String => Object}] The secret values, keyed by name. A
|
|
17
|
+
# value can be a callable, which is called with the name to get the value.
|
|
19
18
|
def initialize(secrets: {})
|
|
20
19
|
@secrets = secrets || {}
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
#
|
|
22
|
+
# Returns whether a secret is registered.
|
|
24
23
|
#
|
|
25
|
-
# @param name [String, Symbol]
|
|
26
|
-
# @return [Boolean] true if the
|
|
24
|
+
# @param name [String, Symbol] The secret name.
|
|
25
|
+
# @return [Boolean] `true` if the secret is registered.
|
|
27
26
|
def include?(name)
|
|
28
27
|
@secrets.include?(name.to_s)
|
|
29
28
|
end
|
|
30
29
|
|
|
31
|
-
#
|
|
30
|
+
# Returns the value of a secret. If the registered value responds to `call`,
|
|
31
|
+
# it's called with the name.
|
|
32
32
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# @param name [String, Symbol] the secret name
|
|
37
|
-
# @return [String] the resolved secret value
|
|
38
|
-
# @raise [SecretNotFoundError] if the secret cannot be resolved
|
|
33
|
+
# @param name [String, Symbol] The secret name.
|
|
34
|
+
# @return [String, nil] The secret value.
|
|
35
|
+
# @raise [SecretNotFoundError] If the secret isn't registered.
|
|
39
36
|
def resolve(name)
|
|
40
37
|
name = name.to_s
|
|
41
38
|
|
|
@@ -48,27 +45,34 @@ module PatientHttp
|
|
|
48
45
|
value&.to_s
|
|
49
46
|
end
|
|
50
47
|
|
|
51
|
-
#
|
|
48
|
+
# Returns a copy of the headers with the secret references replaced by the
|
|
49
|
+
# secret values.
|
|
52
50
|
#
|
|
53
|
-
# @param headers [Hash, nil]
|
|
54
|
-
# @return [Hash, nil]
|
|
51
|
+
# @param headers [Hash, nil] The headers.
|
|
52
|
+
# @return [Hash, nil] The resolved headers.
|
|
53
|
+
# @raise [SecretNotFoundError] If a referenced secret isn't registered.
|
|
55
54
|
def resolve_headers(headers)
|
|
56
55
|
resolve_values(headers)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
#
|
|
58
|
+
# Returns a copy of the query parameters with the secret references
|
|
59
|
+
# replaced by the secret values.
|
|
60
60
|
#
|
|
61
|
-
# @param params [Hash, nil]
|
|
62
|
-
# @return [Hash, nil]
|
|
61
|
+
# @param params [Hash, nil] The query parameters.
|
|
62
|
+
# @return [Hash, nil] The resolved query parameters.
|
|
63
|
+
# @raise [SecretNotFoundError] If a referenced secret isn't registered.
|
|
63
64
|
def resolve_params(params)
|
|
64
65
|
resolve_values(params)
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
#
|
|
68
|
+
# Adds the resolved secret query parameters to a URL.
|
|
68
69
|
#
|
|
69
|
-
# @param url [String]
|
|
70
|
-
# @param secret_params [Hash, nil]
|
|
71
|
-
#
|
|
70
|
+
# @param url [String] The request URL.
|
|
71
|
+
# @param secret_params [Hash, nil] The query parameters whose values are
|
|
72
|
+
# secret references.
|
|
73
|
+
# @return [String] The URL with the parameters added. If there are no
|
|
74
|
+
# parameters, the URL is unchanged.
|
|
75
|
+
# @raise [SecretNotFoundError] If a referenced secret isn't registered.
|
|
72
76
|
def resolve_url(url, secret_params)
|
|
73
77
|
return url if secret_params.nil? || secret_params.empty?
|
|
74
78
|
|
|
@@ -80,8 +84,8 @@ module PatientHttp
|
|
|
80
84
|
|
|
81
85
|
private
|
|
82
86
|
|
|
83
|
-
#
|
|
84
|
-
# values.
|
|
87
|
+
# Returns a copy of a hash with the secret references replaced by the
|
|
88
|
+
# secret values. Other values are unchanged.
|
|
85
89
|
def resolve_values(hash)
|
|
86
90
|
return hash if hash.nil?
|
|
87
91
|
|