patient_http 1.4.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.
@@ -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
@@ -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
- # @return [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
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
@@ -42,6 +53,11 @@ module PatientHttp
42
53
  # to apply to the request when it is sent
43
54
  attr_reader :preprocessors
44
55
 
56
+ # @return [String, nil] Name of the processor that should execute the request.
57
+ # Integrations use this to route the request to a named processor; nil
58
+ # uses the default processor.
59
+ attr_reader :processor
60
+
45
61
  class << self
46
62
  # Reconstruct a Request from a hash
47
63
  #
@@ -56,7 +72,10 @@ module PatientHttp
56
72
  params: load_secret_params(hash["secret_params"]),
57
73
  timeout: hash["timeout"],
58
74
  max_redirects: hash["max_redirects"],
59
- preprocessors: hash["preprocessors"]
75
+ follow_method_changing_redirects: hash["follow_method_changing_redirects"],
76
+ redirect_strip_headers: hash["redirect_strip_headers"],
77
+ preprocessors: hash["preprocessors"],
78
+ processor: hash["processor"]
60
79
  )
61
80
  end
62
81
 
@@ -81,7 +100,7 @@ module PatientHttp
81
100
 
82
101
  # Initializes a new Request.
83
102
  #
84
- # @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).
85
104
  # @param url [String, URI::Generic] The request URL.
86
105
  # @param headers [Hash, HttpHeaders] Request headers.
87
106
  # @param body [String, nil] Request body.
@@ -89,8 +108,16 @@ module PatientHttp
89
108
  # @param params [Hash, nil] Query parameters to append to the URL.
90
109
  # @param timeout [Numeric, nil] Overall timeout in seconds.
91
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}.
92
117
  # @param preprocessors [String, Symbol, Array<String, Symbol>, nil] Names of preprocessors
93
118
  # registered on the configuration to apply to the request when it is sent.
119
+ # @param processor [String, Symbol, nil] Name of the processor that should execute the
120
+ # request. Integrations use this to route the request to a named processor.
94
121
  def initialize(
95
122
  http_method,
96
123
  url,
@@ -100,7 +127,10 @@ module PatientHttp
100
127
  params: nil,
101
128
  timeout: nil,
102
129
  max_redirects: nil,
103
- preprocessors: nil
130
+ follow_method_changing_redirects: nil,
131
+ redirect_strip_headers: nil,
132
+ preprocessors: nil,
133
+ processor: nil
104
134
  )
105
135
  @http_method = http_method.is_a?(String) ? http_method.downcase.to_sym : http_method
106
136
 
@@ -116,7 +146,10 @@ module PatientHttp
116
146
  @body = (body == "") ? nil : body
117
147
  @timeout = timeout
118
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)
119
151
  @preprocessors = normalized_preprocessors(preprocessors)
152
+ @processor = normalized_processor(processor)
120
153
 
121
154
  if json
122
155
  raise ArgumentError.new("Cannot provide both body and json") if @body
@@ -157,7 +190,14 @@ module PatientHttp
157
190
  hash["secret_params"] = @secret_params.transform_values(&:as_json)
158
191
  end
159
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
+
160
199
  hash["preprocessors"] = @preprocessors if @preprocessors.any?
200
+ hash["processor"] = @processor if @processor
161
201
 
162
202
  hash
163
203
  end
@@ -171,6 +211,24 @@ module PatientHttp
171
211
  end
172
212
  end
173
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
+
222
+ # Normalize the processor name to a frozen string or nil.
223
+ def normalized_processor(processor)
224
+ return nil if processor.nil?
225
+
226
+ name = processor.to_s
227
+ raise ArgumentError.new("processor name cannot be empty") if name.empty?
228
+
229
+ name.freeze
230
+ end
231
+
174
232
  # Normalize preprocessor names to a frozen array of strings.
175
233
  def normalized_preprocessors(preprocessors)
176
234
  names = Array(preprocessors).map(&:to_s)
@@ -217,7 +275,7 @@ module PatientHttp
217
275
  raise ArgumentError.new("url must be a String or URI, got: #{@url.class}")
218
276
  end
219
277
 
220
- if %i[get delete].include?(@http_method) && !@body.nil?
278
+ if BODYLESS_METHODS.include?(@http_method) && !@body.nil?
221
279
  raise ArgumentError.new("body is not allowed for #{@http_method.upcase} requests")
222
280
  end
223
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`, `async_delete`, or
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
@@ -116,14 +136,16 @@ module PatientHttp
116
136
  # @param timeout [Float] default timeout in seconds
117
137
  # @param preprocessors [String, Symbol, Array<String, Symbol>, nil] default names of
118
138
  # preprocessors registered on the configuration to apply to requests
139
+ # @param processor [String, Symbol, nil] default processor name for requests
119
140
  # @return [void]
120
- def request_template(base_url: nil, headers: {}, params: nil, timeout: 30, preprocessors: nil)
141
+ def request_template(base_url: nil, headers: {}, params: nil, timeout: 30, preprocessors: nil, processor: nil)
121
142
  @patient_http_request_template = RequestTemplate.new(
122
143
  base_url: base_url,
123
144
  headers: headers,
124
145
  params: params,
125
146
  timeout: timeout,
126
- preprocessors: preprocessors
147
+ preprocessors: preprocessors,
148
+ processor: processor
127
149
  )
128
150
  end
129
151
 
@@ -132,7 +154,7 @@ module PatientHttp
132
154
  # When a request template is configured, the request is built from the template. Otherwise,
133
155
  # it is built directly from the provided arguments.
134
156
  #
135
- # @param method [Symbol] HTTP method (`:get`, `:post`, `:put`, `:patch`, `:delete`)
157
+ # @param method [Symbol] HTTP method (`:get`, `:head`, `:post`, `:put`, `:patch`, `:delete`, `:query`)
136
158
  # @param url [String] absolute URL or path (when using a request template)
137
159
  # @param callback [Class, String] callback class to handle the response
138
160
  # @param headers [Hash, nil] request headers
@@ -143,8 +165,14 @@ module PatientHttp
143
165
  # @param raise_error_responses [Boolean, nil] when true, non-success responses are
144
166
  # reported as errors
145
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
146
172
  # @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
147
173
  # registered on the configuration to apply to the request when it is sent
174
+ # @param processor [String, Symbol, nil] name of the processor that should execute
175
+ # the request
148
176
  # @return [Object] return value from the registered request handler
149
177
  def async_request(
150
178
  method,
@@ -157,10 +185,23 @@ module PatientHttp
157
185
  timeout: nil,
158
186
  raise_error_responses: nil,
159
187
  callback_args: nil,
160
- preprocessors: nil
188
+ follow_method_changing_redirects: nil,
189
+ redirect_strip_headers: nil,
190
+ preprocessors: nil,
191
+ processor: nil
161
192
  )
162
193
  template = async_request_template
163
- kwargs = {body: body, json: json, headers: headers, params: params, timeout: timeout, preprocessors: preprocessors}
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
+ }
164
205
  request = if template
165
206
  template.request(method, url, **kwargs)
166
207
  else
@@ -193,7 +234,7 @@ module PatientHttp
193
234
  #
194
235
  # This delegates to {.ClassMethods#async_request} on the including class.
195
236
  #
196
- # @param method [Symbol] HTTP method (`:get`, `:post`, `:put`, `:patch`, `:delete`)
237
+ # @param method [Symbol] HTTP method (`:get`, `:head`, `:post`, `:put`, `:patch`, `:delete`, `:query`)
197
238
  # @param url [String] absolute URL or path (when using a request template)
198
239
  # @param callback [Class, String] callback class to handle the response
199
240
  # @param headers [Hash, nil] request headers
@@ -204,8 +245,14 @@ module PatientHttp
204
245
  # @param raise_error_responses [Boolean, nil] when true, non-success responses are
205
246
  # reported as errors
206
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
207
252
  # @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
208
253
  # registered on the configuration to apply to the request when it is sent
254
+ # @param processor [String, Symbol, nil] name of the processor that should execute
255
+ # the request
209
256
  # @return [Object] return value from the registered request handler
210
257
  def async_request(
211
258
  method,
@@ -218,7 +265,10 @@ module PatientHttp
218
265
  timeout: nil,
219
266
  raise_error_responses: nil,
220
267
  callback_args: nil,
221
- preprocessors: nil
268
+ follow_method_changing_redirects: nil,
269
+ redirect_strip_headers: nil,
270
+ preprocessors: nil,
271
+ processor: nil
222
272
  )
223
273
  self.class.async_request(
224
274
  method,
@@ -231,7 +281,10 @@ module PatientHttp
231
281
  timeout: timeout,
232
282
  raise_error_responses: raise_error_responses,
233
283
  callback_args: callback_args,
234
- preprocessors: preprocessors
284
+ follow_method_changing_redirects: follow_method_changing_redirects,
285
+ redirect_strip_headers: redirect_strip_headers,
286
+ preprocessors: preprocessors,
287
+ processor: processor
235
288
  )
236
289
  end
237
290
 
@@ -28,6 +28,13 @@ module PatientHttp
28
28
  headers = @config.secret_manager.resolve_headers(request.headers.to_h)
29
29
  headers["x-request-id"] = request_id
30
30
  headers["user-agent"] ||= @config.user_agent if @config.user_agent
31
+ # Compressed responses are inflated by ResponseReader during response
32
+ # decoding rather than by a client middleware wrapper. Requesting gzip is
33
+ # the default because it is what the reader can decode, but a caller that
34
+ # sets the header keeps its own value: "identity" opts out of compression,
35
+ # and any other encoding is delivered still encoded with its
36
+ # content-encoding header intact.
37
+ headers["accept-encoding"] ||= "gzip"
31
38
  url = @config.secret_manager.resolve_url(request.url, request.secret_params)
32
39
 
33
40
  outgoing = OutgoingRequest.new(
@@ -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
- # Determine the HTTP method and body for the redirect
212
- # 301, 302, 303: Convert to GET (no body) - standard browser behavior
213
- # 307, 308: Preserve original method and body
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,7 +247,10 @@ module PatientHttp
236
247
  body: redirect_body,
237
248
  timeout: request.timeout,
238
249
  max_redirects: request.max_redirects,
239
- preprocessors: redirect_preprocessors
250
+ follow_method_changing_redirects: request.follow_method_changing_redirects,
251
+ redirect_strip_headers: request.redirect_strip_headers,
252
+ preprocessors: redirect_preprocessors,
253
+ processor: request.processor
240
254
  )
241
255
 
242
256
  redirect_task_id = "#{id.split("/").first}/#{@redirects.size + 2}"
@@ -34,26 +34,47 @@ module PatientHttp
34
34
  # @param timeout [Float] Default request timeout in seconds
35
35
  # @param preprocessors [String, Symbol, Array<String, Symbol>, nil] Default preprocessors
36
36
  # to apply to all requests
37
- def initialize(base_url: nil, headers: {}, params: nil, timeout: 30, preprocessors: nil)
37
+ # @param processor [String, Symbol, nil] Default processor name for all requests
38
+ def initialize(base_url: nil, headers: {}, params: nil, timeout: 30, preprocessors: nil, processor: nil)
38
39
  @base_url = base_url
39
40
  @headers = HttpHeaders.new(headers)
40
41
  @params = params
41
42
  @timeout = timeout
42
43
  @preprocessors = preprocessors
44
+ @processor = processor
43
45
  end
44
46
 
45
47
  # Build an async HTTP request. Returns a Request object.
46
48
  #
47
- # @param method [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
49
+ # @param method [Symbol] HTTP method (:get, :head, :post, :put, :patch, :delete, :query)
48
50
  # @param uri [String, URI::HTTP] URI path to request (joined with base_url if relative)
49
51
  # @param body [String, nil] request body
50
52
  # @param json [Object, nil] JSON object to serialize (cannot use with body)
51
53
  # @param headers [Hash] additional headers to merge with client headers
52
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
53
60
  # @param preprocessors [String, Symbol, Array<String, Symbol>, nil] preprocessors to apply
54
61
  # to the request (overrides the template default)
62
+ # @param processor [String, Symbol, nil] processor name for the request (overrides the
63
+ # template default)
55
64
  # @return [Request] request object
56
- def request(method, uri, body: nil, json: nil, headers: nil, params: nil, timeout: nil, preprocessors: nil)
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
+ )
57
78
  full_uri = @base_url ? URI.join(@base_url, uri.to_s) : URI(uri)
58
79
 
59
80
  merged_headers = headers&.any? ? @headers.merge(headers) : @headers
@@ -68,7 +89,10 @@ module PatientHttp
68
89
  json: json,
69
90
  params: merged_params,
70
91
  timeout: timeout || @timeout,
71
- preprocessors: preprocessors || @preprocessors
92
+ follow_method_changing_redirects: follow_method_changing_redirects,
93
+ redirect_strip_headers: redirect_strip_headers,
94
+ preprocessors: preprocessors || @preprocessors,
95
+ processor: processor || @processor
72
96
  )
73
97
  end
74
98
 
@@ -81,6 +105,15 @@ module PatientHttp
81
105
  request(:get, uri, **kwargs)
82
106
  end
83
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
+
84
117
  # Convenience method for POST requests.
85
118
  #
86
119
  # @param uri [String, URI::HTTP] URI path to request
@@ -116,5 +149,14 @@ module PatientHttp
116
149
  def delete(uri, **kwargs)
117
150
  request(:delete, uri, **kwargs)
118
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
119
161
  end
120
162
  end