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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +8 -7
- data/CHANGELOG.md +29 -0
- data/README.md +539 -515
- 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 +35 -29
- 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 +21 -5
|
@@ -1,114 +1,120 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
-
#
|
|
9
|
-
#
|
|
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
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
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
|
-
#
|
|
33
|
+
# Runs when the processor starts.
|
|
32
34
|
#
|
|
33
35
|
# @return [void]
|
|
34
36
|
def start
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
#
|
|
39
|
+
# Runs when the processor stops.
|
|
38
40
|
#
|
|
39
41
|
# @return [void]
|
|
40
42
|
def stop
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
#
|
|
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
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# request again
|
|
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]
|
|
74
|
+
# @param request_task [RequestTask] The task.
|
|
70
75
|
# @return [void]
|
|
71
76
|
def request_rejected(request_task)
|
|
72
77
|
end
|
|
73
78
|
|
|
74
|
-
#
|
|
75
|
-
# handler
|
|
76
|
-
# system owns the request again
|
|
77
|
-
#
|
|
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]
|
|
84
|
+
# @param request_task [RequestTask] The task.
|
|
80
85
|
# @return [void]
|
|
81
86
|
def request_requeued(request_task)
|
|
82
87
|
end
|
|
83
88
|
|
|
84
|
-
#
|
|
89
|
+
# Runs when a request starts.
|
|
85
90
|
#
|
|
86
|
-
# @param request_task [RequestTask]
|
|
91
|
+
# @param request_task [RequestTask] The task.
|
|
87
92
|
# @return [void]
|
|
88
93
|
def request_start(request_task)
|
|
89
94
|
end
|
|
90
95
|
|
|
91
|
-
#
|
|
96
|
+
# Runs when a request finishes and its result is delivered.
|
|
92
97
|
#
|
|
93
|
-
# @param request_task [RequestTask]
|
|
98
|
+
# @param request_task [RequestTask] The task.
|
|
94
99
|
# @return [void]
|
|
95
100
|
def request_end(request_task)
|
|
96
101
|
end
|
|
97
102
|
|
|
98
|
-
#
|
|
103
|
+
# Runs when a request fails with an error.
|
|
99
104
|
#
|
|
100
|
-
# @param error [StandardError]
|
|
105
|
+
# @param error [StandardError] The error.
|
|
101
106
|
# @return [void]
|
|
102
107
|
def request_error(error)
|
|
103
108
|
end
|
|
104
109
|
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
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]
|
|
111
|
-
# @param error [StandardError] the delivery
|
|
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
|
-
#
|
|
4
|
-
# Usage: require "patient_http/rails/engine"
|
|
3
|
+
# Require this file to add the Rails integration:
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
#
|
|
5
|
-
#
|
|
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]
|
|
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]
|
|
13
|
+
# @return [Float] The request duration in seconds.
|
|
15
14
|
attr_reader :duration
|
|
16
15
|
|
|
17
|
-
# @return [String]
|
|
16
|
+
# @return [String] The unique request ID.
|
|
18
17
|
attr_reader :request_id
|
|
19
18
|
|
|
20
|
-
# @return [Array<String>] URLs that were
|
|
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
|
-
#
|
|
24
|
+
# Creates an error from its serialized form.
|
|
25
25
|
#
|
|
26
|
-
# @param hash [Hash] hash
|
|
27
|
-
# @return [RedirectError]
|
|
28
|
-
# @raise [ArgumentError]
|
|
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
|
-
#
|
|
47
|
+
# Creates an error.
|
|
47
48
|
#
|
|
48
|
-
# @param message [String]
|
|
49
|
-
# @param url [String]
|
|
50
|
-
# @param http_method [Symbol, String] HTTP method
|
|
51
|
-
# @param duration [Float]
|
|
52
|
-
# @param request_id [String]
|
|
53
|
-
# @param redirects [Array<String>] URLs
|
|
54
|
-
#
|
|
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
|
|
67
|
+
# Returns the error type.
|
|
66
68
|
#
|
|
67
|
-
# @return [Symbol]
|
|
69
|
+
# @return [Symbol] Always `:redirect`.
|
|
68
70
|
def error_type
|
|
69
71
|
:redirect
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
# @return [Class]
|
|
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
|
|
79
|
+
# Returns the callback arguments that were passed with the request.
|
|
78
80
|
#
|
|
79
|
-
# @return [CallbackArgs]
|
|
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
|
-
#
|
|
86
|
+
# Returns the error as a JSON-compatible hash.
|
|
85
87
|
#
|
|
86
|
-
# @return [Hash]
|
|
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
|
-
#
|
|
102
|
+
# The error for a request that exceeds the redirect limit.
|
|
101
103
|
class TooManyRedirectsError < RedirectError
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
# @param
|
|
105
|
-
# @param
|
|
106
|
-
# @param
|
|
107
|
-
# @param
|
|
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
|
-
#
|
|
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
|
-
#
|
|
124
|
-
#
|
|
125
|
-
# @param
|
|
126
|
-
# @param
|
|
127
|
-
# @param
|
|
128
|
-
# @param
|
|
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
|
-
#
|
|
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]
|
|
22
|
-
# @param status [Integer]
|
|
23
|
-
# @return [Symbol]
|
|
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
|
-
#
|
|
35
|
+
# Returns whether following a redirect requires changing the request method.
|
|
36
36
|
#
|
|
37
|
-
# @param http_method [Symbol]
|
|
38
|
-
# @param status [Integer]
|
|
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
|
-
#
|
|
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]
|
|
48
|
-
# @return [Array<String>]
|
|
49
|
-
# @raise [ArgumentError]
|
|
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
|
-
#
|
|
65
|
+
# Returns whether a redirect response should be followed.
|
|
66
66
|
#
|
|
67
|
-
# @param task [RequestTask]
|
|
68
|
-
# @param response_data [Hash]
|
|
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
|
-
#
|
|
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]
|
|
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
|
-
#
|
|
96
|
+
# Builds the task for following a redirect, applying the configured
|
|
97
97
|
# header stripping rules.
|
|
98
98
|
#
|
|
99
|
-
# @param task [RequestTask]
|
|
100
|
-
# @param response_data [Hash]
|
|
101
|
-
# @return [RequestTask]
|
|
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
|
-
#
|
|
110
|
+
# Returns an error if a redirect exceeds the limit or makes a loop.
|
|
111
111
|
#
|
|
112
|
-
# @param task [RequestTask]
|
|
113
|
-
# @param response_data [Hash]
|
|
114
|
-
# @return [RedirectError, nil]
|
|
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
|
-
#
|
|
122
|
+
# Returns whether the redirect count has exceeded the maximum.
|
|
123
123
|
#
|
|
124
|
-
# @param task [RequestTask]
|
|
125
|
-
# @param location [String]
|
|
126
|
-
# @return [TooManyRedirectsError, nil]
|
|
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
|
-
#
|
|
140
|
+
# Returns whether the redirect URL has already been visited (redirect loop).
|
|
141
141
|
#
|
|
142
|
-
# @param task [RequestTask]
|
|
143
|
-
# @param redirect_url [String]
|
|
144
|
-
# @return [RecursiveRedirectError, nil]
|
|
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
|
-
#
|
|
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)
|