patient_http 1.6.1 → 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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +8 -7
  3. data/CHANGELOG.md +29 -0
  4. data/README.md +539 -515
  5. data/VERSION +1 -1
  6. data/lib/patient_http/callback_args.rb +53 -48
  7. data/lib/patient_http/callback_validator.rb +11 -7
  8. data/lib/patient_http/class_helper.rb +6 -7
  9. data/lib/patient_http/client.rb +28 -22
  10. data/lib/patient_http/client_pool.rb +134 -39
  11. data/lib/patient_http/completion_executor.rb +20 -20
  12. data/lib/patient_http/configuration.rb +367 -119
  13. data/lib/patient_http/connection_endpoint.rb +150 -0
  14. data/lib/patient_http/encryptor.rb +28 -18
  15. data/lib/patient_http/error.rb +24 -18
  16. data/lib/patient_http/external_storage.rb +42 -38
  17. data/lib/patient_http/http_error.rb +30 -26
  18. data/lib/patient_http/http_headers.rb +35 -29
  19. data/lib/patient_http/immediate_retries.rb +98 -0
  20. data/lib/patient_http/inline_task_handler.rb +15 -10
  21. data/lib/patient_http/lifecycle_manager.rb +39 -40
  22. data/lib/patient_http/outgoing_request.rb +25 -23
  23. data/lib/patient_http/payload.rb +28 -26
  24. data/lib/patient_http/payload_store/active_record_store.rb +31 -34
  25. data/lib/patient_http/payload_store/base.rb +42 -46
  26. data/lib/patient_http/payload_store/file_store.rb +22 -26
  27. data/lib/patient_http/payload_store/redis_store.rb +28 -34
  28. data/lib/patient_http/payload_store/s3_store.rb +25 -28
  29. data/lib/patient_http/payload_store.rb +2 -0
  30. data/lib/patient_http/processor.rb +111 -79
  31. data/lib/patient_http/processor_observer.rb +65 -59
  32. data/lib/patient_http/rails/engine.rb +13 -8
  33. data/lib/patient_http/redirect_error.rb +50 -41
  34. data/lib/patient_http/redirect_helper.rb +38 -38
  35. data/lib/patient_http/request.rb +70 -46
  36. data/lib/patient_http/request_error.rb +47 -42
  37. data/lib/patient_http/request_helper.rb +142 -119
  38. data/lib/patient_http/request_preparer.rb +13 -10
  39. data/lib/patient_http/request_task.rb +113 -84
  40. data/lib/patient_http/request_template.rb +87 -64
  41. data/lib/patient_http/response.rb +58 -52
  42. data/lib/patient_http/response_reader.rb +66 -65
  43. data/lib/patient_http/secret_manager.rb +34 -30
  44. data/lib/patient_http/secret_reference.rb +33 -26
  45. data/lib/patient_http/synchronous_executor.rb +67 -95
  46. data/lib/patient_http/task_handler.rb +23 -19
  47. data/lib/patient_http/time_helper.rb +8 -8
  48. data/lib/patient_http.rb +311 -186
  49. data/patient_http.gemspec +3 -2
  50. metadata +21 -5
@@ -1,114 +1,120 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PatientHttp
4
- # Interface for observing request processing. A process observer can be registered with
5
- # a Processor and receive events as requests are processed. Observers should be
6
- # lightweight and not do processing other than recording metrics or similar.
4
+ # The base class for objects that receive processor events. Register an
5
+ # observer with {Processor#observe}. Subclass it and override the methods for
6
+ # the events that you need.
7
7
  #
8
- # Hooks run on different threads depending on where the event originates:
9
- # - request_enqueued, request_rejected: the thread calling Processor#enqueue
10
- # (usually an application thread), and the reactor thread for each task
11
- # created to follow a redirect. Work done in these hooks blocks the reactor
12
- # for redirected requests, so keep it off the critical path or accept the
13
- # delay it adds to every other in-flight request
14
- # - capacity_exceeded: the thread calling Processor#enqueue (usually an
15
- # application thread)
16
- # - request_start: the reactor thread
17
- # - request_end, request_error, completion_failed: a completion worker
18
- # thread (request_end also fires on the reactor thread for followed
19
- # redirects, and on the stopping thread for shutdown re-enqueues)
20
- # - request_requeued: the stopping thread or the reactor thread
21
- # - start, stop: the thread calling Processor#start / Processor#stop
8
+ # Keep observers lightweight. Use them to record metrics or to track
9
+ # requests, not to do other work.
22
10
  #
23
- # Observers must be thread-safe. Hooks are called from several threads, and
24
- # the completion-time hooks run on any of the completion worker threads, so
25
- # two of them can run at the same time and in an order unrelated to the
26
- # order the requests completed. Guard any counter or buffer an observer
27
- # shares between calls. Setting completion_threads to 1 serializes the
28
- # completion-time hooks but does not serialize them against the hooks that
29
- # fire on other threads.
11
+ # Each method runs on the thread where its event occurs:
12
+ #
13
+ # - {#request_enqueued} and {#request_rejected}: The thread that calls
14
+ # {Processor#enqueue}, usually an application thread. For a redirect, the
15
+ # reactor thread. Slow work in these methods delays every in-flight request
16
+ # when a redirect is followed.
17
+ # - {#capacity_exceeded}: The thread that calls {Processor#enqueue}.
18
+ # - {#request_start}: The reactor thread.
19
+ # - {#request_end}, {#request_error}, and {#completion_failed}: A completion
20
+ # worker thread. {#request_end} also runs on the reactor thread for a
21
+ # followed redirect, and on the stopping thread for a request that's
22
+ # re-enqueued at shutdown.
23
+ # - {#request_requeued}: The stopping thread or the reactor thread.
24
+ # - {#start} and {#stop}: The thread that calls {Processor#start} or
25
+ # {Processor#stop}.
26
+ #
27
+ # Observers must be thread-safe. Methods run on several threads, and two
28
+ # completion worker threads can call methods at the same time, in any order.
29
+ # Protect any counter or buffer that calls share. If `completion_threads` is
30
+ # 1, the completion worker calls run one at a time, but they can still run at
31
+ # the same time as calls on other threads.
30
32
  class ProcessorObserver
31
- # Called when the processor starts.
33
+ # Runs when the processor starts.
32
34
  #
33
35
  # @return [void]
34
36
  def start
35
37
  end
36
38
 
37
- # Called when the processor stops.
39
+ # Runs when the processor stops.
38
40
  #
39
41
  # @return [void]
40
42
  def stop
41
43
  end
42
44
 
43
- # Called when a request cannot be enqueued because the processor is at capacity.
45
+ # Runs when a request can't be enqueued because the processor is at
46
+ # `max_connections`.
44
47
  #
45
48
  # @return [void]
46
49
  def capacity_exceeded
47
50
  end
48
51
 
49
- # Called when a request task is handed to the processor, before the task is
50
- # visible to the reactor. The notification is guaranteed to arrive before
51
- # request_start for the task, so observers can set up durable tracking
52
- # (e.g. a crash-recovery registry entry) with no risk that the task
53
- # completes first. If the processor does not accept the task,
54
- # request_rejected is sent afterward. Unlike other notifications, an error
55
- # raised here propagates from Processor#enqueue and rejects the task, so a
56
- # failed tracking setup does not let the task be accepted as if it were
57
- # durable.
52
+ # Runs when a task is given to the processor, before the reactor can see
53
+ # the task.
54
+ #
55
+ # This method always runs before {#request_start} for the task. As a
56
+ # result, an observer can set up durable tracking, such as a crash-recovery
57
+ # registry entry, before the task can finish. If the processor doesn't
58
+ # accept the task, {#request_rejected} runs next.
58
59
  #
59
- # @param request_task [RequestTask] the request task that was enqueued
60
+ # Unlike the other methods, an error raised here isn't caught.
61
+ # {Processor#enqueue} raises the error and rejects the task. As a result,
62
+ # the processor never accepts a task whose tracking failed.
63
+ #
64
+ # @param request_task [RequestTask] The task.
60
65
  # @return [void]
61
66
  def request_enqueued(request_task)
62
67
  end
63
68
 
64
- # Called when a request task announced with request_enqueued was not
65
- # accepted by the processor (not running or at capacity). Observers should
66
- # tear down anything they set up in request_enqueued; the caller owns the
67
- # request again once this is sent.
69
+ # Runs when the processor doesn't accept a task after {#request_enqueued},
70
+ # because the processor isn't running or is at capacity. Remove anything
71
+ # that {#request_enqueued} set up. After this call, the caller owns the
72
+ # request again.
68
73
  #
69
- # @param request_task [RequestTask] the request task that was rejected
74
+ # @param request_task [RequestTask] The task.
70
75
  # @return [void]
71
76
  def request_rejected(request_task)
72
77
  end
73
78
 
74
- # Called when an incomplete request task was re-enqueued through its task
75
- # handler (processor shutdown or reactor failure). The task handler's job
76
- # system owns the request again once this is sent, so observers should
77
- # tear down any durable tracking for the task.
79
+ # Runs when a task that didn't finish is re-enqueued through its task
80
+ # handler, because the processor stopped or the reactor failed. After this
81
+ # call, the job system owns the request again, so remove any durable
82
+ # tracking for the task.
78
83
  #
79
- # @param request_task [RequestTask] the request task that was re-enqueued
84
+ # @param request_task [RequestTask] The task.
80
85
  # @return [void]
81
86
  def request_requeued(request_task)
82
87
  end
83
88
 
84
- # Called when a request starts processing.
89
+ # Runs when a request starts.
85
90
  #
86
- # @param request_task [RequestTask] the request task that started
91
+ # @param request_task [RequestTask] The task.
87
92
  # @return [void]
88
93
  def request_start(request_task)
89
94
  end
90
95
 
91
- # Called when a request finishes processing.
96
+ # Runs when a request finishes and its result is delivered.
92
97
  #
93
- # @param request_task [RequestTask] the request task that ended
98
+ # @param request_task [RequestTask] The task.
94
99
  # @return [void]
95
100
  def request_end(request_task)
96
101
  end
97
102
 
98
- # Called when a request encounters an error.
103
+ # Runs when a request fails with an error.
99
104
  #
100
- # @param error [StandardError] the error that occurred
105
+ # @param error [StandardError] The error.
101
106
  # @return [void]
102
107
  def request_error(error)
103
108
  end
104
109
 
105
- # Called when a finished result could not be delivered to the task handler
106
- # after all retries. request_end is NOT sent for the task, so durable
107
- # tracking set up in request_enqueued stays in place and an external
108
- # recovery process (e.g. an orphan collector) can re-enqueue the request.
110
+ # Runs when the task handler can't take a result after all retries.
111
+ #
112
+ # {#request_end} doesn't run for the task. As a result, durable tracking
113
+ # from {#request_enqueued} stays in place, and a recovery process, such as
114
+ # an orphan collector, can re-enqueue the request.
109
115
  #
110
- # @param request_task [RequestTask] the request task whose result was not delivered
111
- # @param error [StandardError] the delivery failure
116
+ # @param request_task [RequestTask] The task.
117
+ # @param error [StandardError] The error from the last delivery attempt.
112
118
  # @return [void]
113
119
  def completion_failed(request_task, error)
114
120
  end
@@ -1,21 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This file must be explicitly required to enable Rails integration.
4
- # Usage: require "patient_http/rails/engine"
3
+ # Require this file to add the Rails integration:
5
4
  #
6
- # This will allow you to install migrations using:
7
- # rails patient_http:install:migrations
5
+ # require "patient_http/rails/engine"
6
+ #
7
+ # Then install the migrations:
8
+ #
9
+ # bin/rails patient_http:install:migrations
8
10
 
9
11
  require "rails/engine"
10
12
 
11
13
  module PatientHttp
14
+ # The Rails integration.
12
15
  module Rails
16
+ # A Rails engine that makes the gem's migrations available to the
17
+ # application. It isn't loaded by default.
18
+ #
19
+ # @example Install the migrations
20
+ # bin/rails patient_http:install:migrations
21
+ # bin/rails db:migrate
13
22
  class Engine < ::Rails::Engine
14
23
  engine_name "patient_http"
15
-
16
- # Migrations will be picked up automatically from db/migrate
17
- # when the engine is loaded. Users can copy them using:
18
- # rails patient_http:install:migrations
19
24
  end
20
25
  end
21
26
  end
@@ -1,31 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PatientHttp
4
- # Base class for redirect-related errors.
5
- # These errors occur when redirect handling fails due to too many redirects
6
- # or a redirect loop.
4
+ # The base class for redirect errors. A redirect error occurs when a request
5
+ # exceeds the redirect limit or finds a redirect loop.
7
6
  class RedirectError < Error
8
- # @return [String] Request URL
7
+ # @return [String] The request URL.
9
8
  attr_reader :url
10
9
 
11
- # @return [Symbol] HTTP method
10
+ # @return [Symbol] The HTTP method.
12
11
  attr_reader :http_method
13
12
 
14
- # @return [Float] Request duration in seconds
13
+ # @return [Float] The request duration in seconds.
15
14
  attr_reader :duration
16
15
 
17
- # @return [String] Unique request identifier
16
+ # @return [String] The unique request ID.
18
17
  attr_reader :request_id
19
18
 
20
- # @return [Array<String>] URLs that were visited during redirect chain
19
+ # @return [Array<String>] The URLs of the redirects that were followed, in
20
+ # order.
21
21
  attr_reader :redirects
22
22
 
23
23
  class << self
24
- # Reconstruct a RedirectError from a hash
24
+ # Creates an error from its serialized form.
25
25
  #
26
- # @param hash [Hash] hash representation
27
- # @return [RedirectError] reconstructed error
28
- # @raise [ArgumentError] if the serialized error class is not a RedirectError
26
+ # @param hash [Hash] The hash from {#as_json}.
27
+ # @return [RedirectError] The error.
28
+ # @raise [ArgumentError] If the serialized error class isn't a
29
+ # `RedirectError`.
29
30
  def load(hash)
30
31
  error_class = ClassHelper.resolve_class_name(hash["error_class"])
31
32
  unless error_class.is_a?(Class) && error_class <= RedirectError
@@ -43,15 +44,16 @@ module PatientHttp
43
44
  end
44
45
  end
45
46
 
46
- # Initializes a new RedirectError.
47
+ # Creates an error.
47
48
  #
48
- # @param message [String] Error message
49
- # @param url [String] Request URL
50
- # @param http_method [Symbol, String] HTTP method
51
- # @param duration [Float] Request duration in seconds
52
- # @param request_id [String] Unique request identifier
53
- # @param redirects [Array<String>] URLs visited during redirect chain
54
- # @param callback_args [Hash, nil] callback arguments (string keys)
49
+ # @param message [String] The error message.
50
+ # @param url [String] The request URL.
51
+ # @param http_method [Symbol, String] The HTTP method.
52
+ # @param duration [Float] The request duration in seconds.
53
+ # @param request_id [String] The unique request ID.
54
+ # @param redirects [Array<String>] The URLs of the redirects that were
55
+ # followed.
56
+ # @param callback_args [Hash, nil] The callback arguments, with string keys.
55
57
  def initialize(message, url:, http_method:, duration:, request_id:, redirects:, callback_args: nil)
56
58
  super(message)
57
59
  @url = url
@@ -62,28 +64,28 @@ module PatientHttp
62
64
  @callback_args_data = callback_args || {}
63
65
  end
64
66
 
65
- # Returns the error type symbol.
67
+ # Returns the error type.
66
68
  #
67
- # @return [Symbol] the error type
69
+ # @return [Symbol] Always `:redirect`.
68
70
  def error_type
69
71
  :redirect
70
72
  end
71
73
 
72
- # @return [Class] the class of the exception. This is for compatibility with RequestError.
74
+ # @return [Class] The class of this error.
73
75
  def error_class
74
76
  self.class
75
77
  end
76
78
 
77
- # Returns the callback arguments as a CallbackArgs object.
79
+ # Returns the callback arguments that were passed with the request.
78
80
  #
79
- # @return [CallbackArgs] the callback arguments
81
+ # @return [CallbackArgs] The callback arguments.
80
82
  def callback_args
81
83
  @callback_args ||= CallbackArgs.load(@callback_args_data)
82
84
  end
83
85
 
84
- # Convert to hash with string keys for serialization
86
+ # Returns the error as a JSON-compatible hash.
85
87
  #
86
- # @return [Hash] hash representation
88
+ # @return [Hash] The serialized error.
87
89
  def as_json
88
90
  {
89
91
  "error_class" => self.class.name,
@@ -97,14 +99,17 @@ module PatientHttp
97
99
  end
98
100
  end
99
101
 
100
- # Error raised when too many redirects are encountered.
102
+ # The error for a request that exceeds the redirect limit.
101
103
  class TooManyRedirectsError < RedirectError
102
- # @param url [String] The URL that would have been redirected to
103
- # @param http_method [Symbol, String] HTTP method
104
- # @param duration [Float] Request duration in seconds
105
- # @param request_id [String] Unique request identifier
106
- # @param redirects [Array<String>] URLs visited during redirect chain
107
- # @param callback_args [Hash, nil] callback arguments (string keys)
104
+ # Creates an error.
105
+ #
106
+ # @param url [String] The URL of the redirect that exceeded the limit.
107
+ # @param http_method [Symbol, String] The HTTP method.
108
+ # @param duration [Float] The request duration in seconds.
109
+ # @param request_id [String] The unique request ID.
110
+ # @param redirects [Array<String>] The URLs of the redirects that were
111
+ # followed.
112
+ # @param callback_args [Hash, nil] The callback arguments, with string keys.
108
113
  def initialize(url:, http_method:, duration:, request_id:, redirects:, callback_args: nil)
109
114
  super(
110
115
  "Too many redirects (#{redirects.size}) while requesting #{http_method.to_s.upcase} #{redirects.first || url}",
@@ -118,14 +123,18 @@ module PatientHttp
118
123
  end
119
124
  end
120
125
 
121
- # Error raised when a recursive redirect is detected.
126
+ # The error for a redirect loop, where a redirect goes to a URL that the
127
+ # request already visited.
122
128
  class RecursiveRedirectError < RedirectError
123
- # @param url [String] The URL that caused the loop
124
- # @param http_method [Symbol, String] HTTP method
125
- # @param duration [Float] Request duration in seconds
126
- # @param request_id [String] Unique request identifier
127
- # @param redirects [Array<String>] URLs visited during redirect chain
128
- # @param callback_args [Hash, nil] callback arguments (string keys)
129
+ # Creates an error.
130
+ #
131
+ # @param url [String] The URL that caused the loop.
132
+ # @param http_method [Symbol, String] The HTTP method.
133
+ # @param duration [Float] The request duration in seconds.
134
+ # @param request_id [String] The unique request ID.
135
+ # @param redirects [Array<String>] The URLs of the redirects that were
136
+ # followed.
137
+ # @param callback_args [Hash, nil] The callback arguments, with string keys.
129
138
  def initialize(url:, http_method:, duration:, request_id:, redirects:, callback_args: nil)
130
139
  super(
131
140
  "Recursive redirect detected: #{url} was already visited in redirect chain",
@@ -8,7 +8,7 @@ module PatientHttp
8
8
  # @api private
9
9
  module RedirectHelper
10
10
  class << self
11
- # Determine the HTTP method to use when following a redirect.
11
+ # Returns the HTTP method to use when following a redirect.
12
12
  #
13
13
  # The rules follow RFC 9110 and the WHATWG Fetch standard:
14
14
  #
@@ -18,9 +18,9 @@ module PatientHttp
18
18
  # - 303 preserves GET and HEAD; every other method becomes GET.
19
19
  # - 300, 307, and 308 preserve the method.
20
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
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
24
  def redirect_method(http_method, status)
25
25
  case status
26
26
  when 301, 302
@@ -32,21 +32,21 @@ module PatientHttp
32
32
  end
33
33
  end
34
34
 
35
- # Check if following a redirect requires changing the request method.
35
+ # Returns whether following a redirect requires changing the request method.
36
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
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
40
  def method_change_required?(http_method, status)
41
41
  redirect_method(http_method, status) != http_method
42
42
  end
43
43
 
44
- # Normalize header names used to strip headers from redirected requests.
44
+ # Normalizes header names used to strip headers from redirected requests.
45
45
  # Names are downcased so they match header names case insensitively.
46
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
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
50
  def normalize_header_names(names)
51
51
  Array(names).map do |name|
52
52
  unless name.is_a?(String) || name.is_a?(Symbol)
@@ -62,11 +62,11 @@ module PatientHttp
62
62
 
63
63
  private
64
64
 
65
- # Check if a redirect response should be followed.
65
+ # Returns whether a redirect response should be followed.
66
66
  #
67
- # @param task [RequestTask] the request task
68
- # @param response_data [Hash] the response data with status, headers, body
69
- # @return [Boolean] true if the redirect should be followed
67
+ # @param task [RequestTask] The request task.
68
+ # @param response_data [Hash] The response data with status, headers, body.
69
+ # @return [Boolean] `true` if the redirect should be followed.
70
70
  def should_follow_redirect?(task, response_data)
71
71
  status = response_data[:status]
72
72
  return false unless FOLLOWABLE_REDIRECT_STATUSES.include?(status)
@@ -82,10 +82,10 @@ module PatientHttp
82
82
  true
83
83
  end
84
84
 
85
- # Check if the request may change its method to follow a redirect.
85
+ # Returns whether the request may change its method to follow a redirect.
86
86
  # The request setting takes precedence over the configuration.
87
87
  #
88
- # @param task [RequestTask] the request task
88
+ # @param task [RequestTask] The request task.
89
89
  # @return [Boolean]
90
90
  def follow_method_changing_redirect?(task)
91
91
  value = task.request.follow_method_changing_redirects
@@ -93,12 +93,12 @@ module PatientHttp
93
93
  value
94
94
  end
95
95
 
96
- # Build the task for following a redirect, applying the configured
96
+ # Builds the task for following a redirect, applying the configured
97
97
  # header stripping rules.
98
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
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
102
  def build_redirect_task(task, response_data)
103
103
  task.redirect_task(
104
104
  location: response_data[:headers]["location"],
@@ -107,11 +107,11 @@ module PatientHttp
107
107
  )
108
108
  end
109
109
 
110
- # Check for either too-many-redirects or recursive redirect.
110
+ # Returns an error if a redirect exceeds the limit or makes a loop.
111
111
  #
112
- # @param task [RequestTask] the request task
113
- # @param response_data [Hash] the response data with status, headers, body
114
- # @return [RedirectError, nil] error if redirect should not proceed, nil otherwise
112
+ # @param task [RequestTask] The request task.
113
+ # @param response_data [Hash] The response data with status, headers, body.
114
+ # @return [RedirectError, nil] Error if redirect should not proceed, nil otherwise.
115
115
  def check_redirect_error(task, response_data)
116
116
  location = response_data[:headers]["location"]
117
117
  redirect_url = resolve_redirect_url(task.request.url, location)
@@ -119,11 +119,11 @@ module PatientHttp
119
119
  check_too_many_redirects(task, location) || check_recursive_redirect(task, redirect_url)
120
120
  end
121
121
 
122
- # Check if the redirect count has exceeded the maximum.
122
+ # Returns whether the redirect count has exceeded the maximum.
123
123
  #
124
- # @param task [RequestTask] the request task
125
- # @param location [String] the redirect location URL
126
- # @return [TooManyRedirectsError, nil] error if exceeded, nil otherwise
124
+ # @param task [RequestTask] The request task.
125
+ # @param location [String] The redirect location URL.
126
+ # @return [TooManyRedirectsError, nil] Error if exceeded, nil otherwise.
127
127
  def check_too_many_redirects(task, location)
128
128
  return nil if task.redirects.size < task.max_redirects
129
129
 
@@ -137,11 +137,11 @@ module PatientHttp
137
137
  )
138
138
  end
139
139
 
140
- # Check if the redirect URL has already been visited (redirect loop).
140
+ # Returns whether the redirect URL has already been visited (redirect loop).
141
141
  #
142
- # @param task [RequestTask] the request task
143
- # @param redirect_url [String] the resolved redirect URL
144
- # @return [RecursiveRedirectError, nil] error if loop detected, nil otherwise
142
+ # @param task [RequestTask] The request task.
143
+ # @param redirect_url [String] The resolved redirect URL.
144
+ # @return [RecursiveRedirectError, nil] Error if loop detected, nil otherwise.
145
145
  def check_recursive_redirect(task, redirect_url)
146
146
  visited_urls = task.redirects + [task.request.url]
147
147
  return nil unless visited_urls.include?(redirect_url)
@@ -156,11 +156,11 @@ module PatientHttp
156
156
  )
157
157
  end
158
158
 
159
- # Resolve a redirect URL, handling relative URLs.
159
+ # Resolves a redirect URL, handling relative URLs.
160
160
  #
161
- # @param base_url [String] The base URL
162
- # @param location [String] The Location header value
163
- # @return [String] The resolved absolute URL
161
+ # @param base_url [String] The base URL.
162
+ # @param location [String] The Location header value.
163
+ # @return [String] The resolved absolute URL.
164
164
  def resolve_redirect_url(base_url, location)
165
165
  base_uri = URI.parse(base_url)
166
166
  redirect_uri = URI.parse(location)