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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module PatientHttp
|
|
7
|
+
# Wraps an HTTP endpoint to configure each connection as it is established.
|
|
8
|
+
#
|
|
9
|
+
# When a connection timeout is given, establishing the connection (the TCP
|
|
10
|
+
# connect and the TLS handshake) is bounded by a fiber scheduler timeout that
|
|
11
|
+
# raises `IO::TimeoutError`. The timeout is not set as the socket's
|
|
12
|
+
# `IO#timeout`, which would also limit every later read and write for as long
|
|
13
|
+
# as the connection lives, so once the connection is established the request
|
|
14
|
+
# timeout alone governs the exchange.
|
|
15
|
+
#
|
|
16
|
+
# When TCP keepalive settings are given, they are applied to each TCP socket.
|
|
17
|
+
# Keepalive probes keep the mappings of NAT gateways and stateful firewalls
|
|
18
|
+
# alive while a pooled connection is idle, and let the kernel detect a dead
|
|
19
|
+
# peer so the connection is retired before a request is sent on it.
|
|
20
|
+
#
|
|
21
|
+
# When a TCP user timeout is given, the kernel aborts a connection whose
|
|
22
|
+
# transmitted data stays unacknowledged for that long, so a request sent to a
|
|
23
|
+
# peer that has silently gone away fails quickly instead of waiting for the
|
|
24
|
+
# request timeout. Data the peer has acknowledged is not affected, so a slow
|
|
25
|
+
# response is never cut short. Only Linux supports this option.
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
class ConnectionEndpoint < SimpleDelegator
|
|
29
|
+
# @return [Numeric, nil] Seconds allowed to establish a connection.
|
|
30
|
+
attr_reader :connection_timeout
|
|
31
|
+
|
|
32
|
+
# @return [Hash, nil] The keepalive settings with :idle, :interval, and :count.
|
|
33
|
+
attr_reader :tcp_keepalive
|
|
34
|
+
|
|
35
|
+
# @return [Numeric, nil] Seconds transmitted data may stay unacknowledged.
|
|
36
|
+
attr_reader :tcp_user_timeout
|
|
37
|
+
|
|
38
|
+
# @param endpoint [Async::HTTP::Endpoint] The endpoint to wrap.
|
|
39
|
+
# @param connection_timeout [Numeric, nil] Seconds allowed to establish a
|
|
40
|
+
# connection, or nil for no limit beyond the endpoint's own.
|
|
41
|
+
# @param tcp_keepalive [Hash, nil] Keepalive settings with :idle, :interval, and
|
|
42
|
+
# :count in seconds and probes (:interval and :count optional), or nil to leave
|
|
43
|
+
# the kernel defaults.
|
|
44
|
+
# @param tcp_user_timeout [Numeric, nil] Seconds transmitted data may stay
|
|
45
|
+
# unacknowledged, or nil to leave the kernel default.
|
|
46
|
+
def initialize(endpoint, connection_timeout: nil, tcp_keepalive: nil, tcp_user_timeout: nil)
|
|
47
|
+
super(endpoint)
|
|
48
|
+
@connection_timeout = connection_timeout
|
|
49
|
+
@tcp_keepalive = tcp_keepalive
|
|
50
|
+
@tcp_user_timeout = tcp_user_timeout
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Connects to the wrapped endpoint and configures the socket.
|
|
54
|
+
#
|
|
55
|
+
# @yield [socket] The connected socket, closed when the block returns.
|
|
56
|
+
# @return [IO] The connected socket when no block is given.
|
|
57
|
+
def connect
|
|
58
|
+
socket = connect_within_timeout
|
|
59
|
+
begin
|
|
60
|
+
apply_tcp_keepalive(socket)
|
|
61
|
+
apply_tcp_user_timeout(socket)
|
|
62
|
+
rescue
|
|
63
|
+
socket.close
|
|
64
|
+
raise
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return socket unless block_given?
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
yield socket
|
|
71
|
+
ensure
|
|
72
|
+
socket.close
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def connect_within_timeout
|
|
79
|
+
task = ::Async::Task.current?
|
|
80
|
+
return __getobj__.connect unless @connection_timeout && task
|
|
81
|
+
|
|
82
|
+
task.with_timeout(@connection_timeout, ::IO::TimeoutError, "Connect timed out") do
|
|
83
|
+
__getobj__.connect
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Keepalive is a TCP feature, so it is skipped for the socket pair behind a
|
|
88
|
+
# proxy tunnel. The kernel constants differ by platform: Linux names the idle
|
|
89
|
+
# time TCP_KEEPIDLE, macOS names it TCP_KEEPALIVE. A socket that rejects an
|
|
90
|
+
# option keeps working without it, and an interval or count that is not
|
|
91
|
+
# given keeps the kernel default.
|
|
92
|
+
#
|
|
93
|
+
# Top-level constants are written with a leading `::` because Delegator
|
|
94
|
+
# descends from BasicObject, where `defined?` cannot see them.
|
|
95
|
+
def apply_tcp_keepalive(socket)
|
|
96
|
+
return unless @tcp_keepalive
|
|
97
|
+
|
|
98
|
+
raw_socket = tcp_socket(socket)
|
|
99
|
+
return unless raw_socket
|
|
100
|
+
|
|
101
|
+
raw_socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)
|
|
102
|
+
if (idle_option = keepalive_idle_option)
|
|
103
|
+
raw_socket.setsockopt(::Socket::IPPROTO_TCP, idle_option, @tcp_keepalive[:idle])
|
|
104
|
+
end
|
|
105
|
+
interval = @tcp_keepalive[:interval]
|
|
106
|
+
if interval && defined?(::Socket::TCP_KEEPINTVL)
|
|
107
|
+
raw_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_KEEPINTVL, interval)
|
|
108
|
+
end
|
|
109
|
+
count = @tcp_keepalive[:count]
|
|
110
|
+
if count && defined?(::Socket::TCP_KEEPCNT)
|
|
111
|
+
raw_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_KEEPCNT, count)
|
|
112
|
+
end
|
|
113
|
+
rescue ::SystemCallError
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def keepalive_idle_option
|
|
118
|
+
if defined?(::Socket::TCP_KEEPIDLE)
|
|
119
|
+
::Socket::TCP_KEEPIDLE
|
|
120
|
+
elsif defined?(::Socket::TCP_KEEPALIVE)
|
|
121
|
+
::Socket::TCP_KEEPALIVE
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The kernel takes the user timeout in milliseconds, and a value of zero
|
|
126
|
+
# restores its default instead of enforcing a limit, so a positive duration
|
|
127
|
+
# is rounded up to at least one millisecond. Platforms without the option
|
|
128
|
+
# keep their default retransmission limits.
|
|
129
|
+
def apply_tcp_user_timeout(socket)
|
|
130
|
+
return unless @tcp_user_timeout && defined?(::Socket::TCP_USER_TIMEOUT)
|
|
131
|
+
|
|
132
|
+
raw_socket = tcp_socket(socket)
|
|
133
|
+
return unless raw_socket
|
|
134
|
+
|
|
135
|
+
milliseconds = (@tcp_user_timeout * 1000).ceil
|
|
136
|
+
raw_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_USER_TIMEOUT, milliseconds)
|
|
137
|
+
rescue ::SystemCallError
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The TCP socket behind a plain or TLS connection, or nil for a socket that
|
|
142
|
+
# is not TCP, such as the socket pair behind a proxy tunnel.
|
|
143
|
+
def tcp_socket(socket)
|
|
144
|
+
raw_socket = socket.respond_to?(:to_io) ? socket.to_io : socket
|
|
145
|
+
return nil unless raw_socket.is_a?(::BasicSocket) && raw_socket.local_address.ip?
|
|
146
|
+
|
|
147
|
+
raw_socket
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# Encrypts payloads before they're stored in a job queue, and decrypts them
|
|
5
|
+
# after they're read.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# The encryptor serializes a hash to JSON, passes the bytes to the encryption
|
|
8
|
+
# callable, and stores the Base64-encoded result as
|
|
9
|
+
# `{"__encrypted__" => true, "value" => "<base64>"}`. You can use any
|
|
10
|
+
# encryption library.
|
|
11
|
+
#
|
|
12
|
+
# @see Configuration#encryptor
|
|
9
13
|
class Encryptor
|
|
10
|
-
#
|
|
14
|
+
# Creates an encryptor. Without callables, the encryptor returns data
|
|
15
|
+
# unchanged.
|
|
11
16
|
#
|
|
12
|
-
# @param encryption [#call, nil]
|
|
13
|
-
#
|
|
17
|
+
# @param encryption [#call, nil] An object that takes the bytes as a String
|
|
18
|
+
# and returns the encrypted bytes.
|
|
19
|
+
# @param decryption [#call, nil] An object that takes the encrypted bytes and
|
|
20
|
+
# returns the decrypted bytes.
|
|
14
21
|
def initialize(encryption: nil, decryption: nil)
|
|
15
22
|
@encryption = encryption
|
|
16
23
|
@decryption = decryption
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
26
|
+
# Encrypts a hash. If no encryption callable is set, the hash is returned
|
|
27
|
+
# unchanged.
|
|
21
28
|
#
|
|
22
|
-
# @param data [Hash] The data to
|
|
23
|
-
# @return [Hash, nil] The encrypted data
|
|
24
|
-
#
|
|
29
|
+
# @param data [Hash, nil] The data to encrypt.
|
|
30
|
+
# @return [Hash, nil] The encrypted data, or the original data if no
|
|
31
|
+
# encryption callable is set.
|
|
32
|
+
# @raise [ArgumentError] If the data isn't a Hash or `nil`.
|
|
33
|
+
# @raise [JSON::GeneratorError] If the data can't be serialized to JSON.
|
|
25
34
|
def encrypt(data)
|
|
26
35
|
return nil if data.nil?
|
|
27
36
|
|
|
@@ -37,13 +46,14 @@ module PatientHttp
|
|
|
37
46
|
}
|
|
38
47
|
end
|
|
39
48
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
49
|
+
# Decrypts a hash. If no decryption callable is set, or if the hash isn't
|
|
50
|
+
# encrypted, the hash is returned unchanged. As a result, data written
|
|
51
|
+
# before encryption was turned on can still be read.
|
|
42
52
|
#
|
|
43
|
-
# @param data [Hash] The data to
|
|
44
|
-
# @return [Hash, nil] The decrypted data
|
|
45
|
-
#
|
|
46
|
-
# @raise [JSON::ParserError] If the decrypted data
|
|
53
|
+
# @param data [Hash, nil] The data to decrypt.
|
|
54
|
+
# @return [Hash, nil] The decrypted data, or the original data.
|
|
55
|
+
# @raise [ArgumentError] If the data isn't a Hash or `nil`.
|
|
56
|
+
# @raise [JSON::ParserError] If the decrypted data isn't valid JSON.
|
|
47
57
|
def decrypt(data)
|
|
48
58
|
return nil if data.nil?
|
|
49
59
|
|
data/lib/patient_http/error.rb
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
5
|
-
# defines the
|
|
4
|
+
# The abstract base class for errors that a request passes to the `on_error`
|
|
5
|
+
# callback. It defines the methods that all of these errors have.
|
|
6
|
+
#
|
|
7
|
+
# The subclasses are {HttpError}, {RedirectError}, and {RequestError}.
|
|
6
8
|
class Error < StandardError
|
|
7
9
|
class << self
|
|
8
|
-
#
|
|
10
|
+
# Creates an error from its serialized form. The hash determines the
|
|
11
|
+
# subclass.
|
|
9
12
|
#
|
|
10
|
-
# @param hash [Hash] hash
|
|
11
|
-
# @return [Error]
|
|
13
|
+
# @param hash [Hash] The hash from {#as_json}.
|
|
14
|
+
# @return [Error] The error.
|
|
12
15
|
def load(hash)
|
|
13
16
|
# Dispatch based on hash structure
|
|
14
17
|
if hash.key?("response")
|
|
@@ -21,54 +24,57 @@ module PatientHttp
|
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
|
|
24
|
-
# Returns the error type
|
|
27
|
+
# Returns the error type. Subclasses return a more specific value.
|
|
25
28
|
#
|
|
26
|
-
# @return [Symbol]
|
|
29
|
+
# @return [Symbol] The error type.
|
|
27
30
|
def error_type
|
|
28
31
|
:unknown
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
# @return [String]
|
|
34
|
+
# @return [String] The request URL.
|
|
32
35
|
def url
|
|
33
36
|
raise NotImplementedError, "Subclasses must implement #url"
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
# @return [Symbol] HTTP method
|
|
39
|
+
# @return [Symbol] The HTTP method.
|
|
37
40
|
def http_method
|
|
38
41
|
raise NotImplementedError, "Subclasses must implement #http_method"
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
# @return [Float]
|
|
44
|
+
# @return [Float] The request duration in seconds.
|
|
42
45
|
def duration
|
|
43
46
|
raise NotImplementedError, "Subclasses must implement #duration"
|
|
44
47
|
end
|
|
45
48
|
|
|
46
|
-
# @return [String]
|
|
49
|
+
# @return [String] The unique request ID.
|
|
47
50
|
def request_id
|
|
48
51
|
raise NotImplementedError, "Subclasses must implement #request_id"
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
# @return [Class]
|
|
54
|
+
# @return [Class] The class of the exception that caused the error.
|
|
52
55
|
def error_class
|
|
53
56
|
raise NotImplementedError, "Subclasses must implement #error_class"
|
|
54
57
|
end
|
|
55
58
|
|
|
56
|
-
# @return [CallbackArgs]
|
|
59
|
+
# @return [CallbackArgs] The callback arguments that were passed with the
|
|
60
|
+
# request.
|
|
57
61
|
def callback_args
|
|
58
62
|
raise NotImplementedError, "Subclasses must implement #callback_args"
|
|
59
63
|
end
|
|
60
64
|
|
|
61
|
-
#
|
|
65
|
+
# Returns the error as a JSON-compatible hash. Subclasses must implement
|
|
66
|
+
# this method.
|
|
62
67
|
#
|
|
63
|
-
# @return [Hash]
|
|
68
|
+
# @return [Hash] The serialized error.
|
|
64
69
|
def as_json
|
|
65
70
|
raise NotImplementedError, "Subclasses must implement #as_json"
|
|
66
71
|
end
|
|
67
72
|
|
|
68
|
-
#
|
|
73
|
+
# Returns the error as a JSON string.
|
|
69
74
|
#
|
|
70
|
-
# @param options [Hash] options
|
|
71
|
-
#
|
|
75
|
+
# @param options [Hash, nil] The options for `JSON.generate`. This parameter
|
|
76
|
+
# makes the method compatible with Active Support.
|
|
77
|
+
# @return [String] The JSON string.
|
|
72
78
|
def to_json(options = nil)
|
|
73
79
|
JSON.generate(as_json, options)
|
|
74
80
|
end
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# Stores large payloads in the registered payload store, and replaces them
|
|
5
|
+
# with a reference.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# A reference has the form `{"$ref" => {"store" => name, "key" => key}}`. This
|
|
8
|
+
# class works with any JSON-compatible hash, such as a serialized {Request},
|
|
9
|
+
# {Response}, or {Error}.
|
|
9
10
|
#
|
|
10
|
-
# @example
|
|
11
|
+
# @example Store a payload
|
|
11
12
|
# external_storage = ExternalStorage.new(config)
|
|
12
13
|
# data = response.as_json
|
|
13
14
|
# stored_data = external_storage.store(data)
|
|
14
15
|
#
|
|
15
|
-
# @example
|
|
16
|
+
# @example Fetch and delete a payload
|
|
16
17
|
# if ExternalStorage.storage_ref?(data)
|
|
17
18
|
# external_storage = ExternalStorage.new(config)
|
|
18
19
|
# original_data = external_storage.fetch(data)
|
|
@@ -20,59 +21,61 @@ module PatientHttp
|
|
|
20
21
|
# external_storage.delete(data)
|
|
21
22
|
# end
|
|
22
23
|
class ExternalStorage
|
|
23
|
-
#
|
|
24
|
+
# The key that identifies a storage reference in serialized JSON.
|
|
24
25
|
REFERENCE_KEY = "$ref"
|
|
25
26
|
|
|
27
|
+
# Raised when no payload store is registered, or when a reference names a
|
|
28
|
+
# store that isn't registered.
|
|
26
29
|
class PayloadStoreNotFoundError < StandardError; end
|
|
30
|
+
|
|
31
|
+
# Raised when a referenced payload isn't in the store.
|
|
27
32
|
class PayloadNotFoundError < StandardError; end
|
|
28
33
|
|
|
29
34
|
class << self
|
|
30
|
-
#
|
|
35
|
+
# Returns whether data is a storage reference.
|
|
31
36
|
#
|
|
32
|
-
# @param data [Hash, Object]
|
|
33
|
-
# @return [Boolean] true if
|
|
37
|
+
# @param data [Hash, Object] The data to check.
|
|
38
|
+
# @return [Boolean] `true` if the data is a storage reference.
|
|
34
39
|
def storage_ref?(data)
|
|
35
40
|
data.is_a?(Hash) && data.key?(REFERENCE_KEY)
|
|
36
41
|
end
|
|
37
42
|
end
|
|
38
43
|
|
|
39
|
-
# @return [Configuration] the
|
|
44
|
+
# @return [Configuration] The configuration that has the payload stores.
|
|
40
45
|
attr_reader :config
|
|
41
46
|
|
|
42
|
-
#
|
|
47
|
+
# Creates an external storage object.
|
|
43
48
|
#
|
|
44
|
-
# @param config [Configuration] the
|
|
49
|
+
# @param config [Configuration] The configuration that has the payload
|
|
50
|
+
# stores.
|
|
45
51
|
def initialize(config)
|
|
46
52
|
@config = config
|
|
47
53
|
end
|
|
48
54
|
|
|
49
|
-
#
|
|
55
|
+
# Returns whether data is a storage reference.
|
|
50
56
|
#
|
|
51
|
-
# @param data [Hash, Object]
|
|
52
|
-
# @return [Boolean] true if
|
|
57
|
+
# @param data [Hash, Object] The data to check.
|
|
58
|
+
# @return [Boolean] `true` if the data is a storage reference.
|
|
53
59
|
def storage_ref?(data)
|
|
54
60
|
self.class.storage_ref?(data)
|
|
55
61
|
end
|
|
56
62
|
|
|
57
|
-
#
|
|
63
|
+
# Returns whether a payload store is registered.
|
|
58
64
|
#
|
|
59
|
-
# @return [Boolean] true if
|
|
65
|
+
# @return [Boolean] `true` if a payload store is registered.
|
|
60
66
|
def enabled?
|
|
61
67
|
!!config.payload_store
|
|
62
68
|
end
|
|
63
69
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# If no payload store is configured, or if the hash is below the
|
|
67
|
-
# threshold, the original hash is returned unchanged.
|
|
70
|
+
# Stores a hash in the default payload store, and returns a reference to
|
|
71
|
+
# it.
|
|
68
72
|
#
|
|
69
|
-
# @param data [Hash]
|
|
70
|
-
# @param max_size [Integer, nil]
|
|
71
|
-
#
|
|
72
|
-
# If
|
|
73
|
-
#
|
|
74
|
-
# @
|
|
75
|
-
# @raise [PayloadStoreNotFoundError] If no payload store is configured
|
|
73
|
+
# @param data [Hash] The hash to store.
|
|
74
|
+
# @param max_size [Integer, nil] The size in bytes above which the hash is
|
|
75
|
+
# stored. If the JSON is this size or smaller, the original hash is
|
|
76
|
+
# returned. If `nil`, the hash is always stored.
|
|
77
|
+
# @return [Hash] The reference, or the original hash if it wasn't stored.
|
|
78
|
+
# @raise [PayloadStoreNotFoundError] If no payload store is registered.
|
|
76
79
|
def store(data, max_size: nil)
|
|
77
80
|
store = config.payload_store
|
|
78
81
|
raise PayloadStoreNotFoundError.new("No payload store configured") unless store
|
|
@@ -91,12 +94,13 @@ module PatientHttp
|
|
|
91
94
|
}
|
|
92
95
|
end
|
|
93
96
|
|
|
94
|
-
#
|
|
97
|
+
# Fetches a stored hash.
|
|
95
98
|
#
|
|
96
|
-
# @param data [Hash]
|
|
97
|
-
# @return [Hash]
|
|
98
|
-
# @raise [
|
|
99
|
-
# @raise [
|
|
99
|
+
# @param data [Hash] The reference.
|
|
100
|
+
# @return [Hash] The stored hash.
|
|
101
|
+
# @raise [ArgumentError] If the data isn't a storage reference.
|
|
102
|
+
# @raise [PayloadStoreNotFoundError] If the store isn't registered.
|
|
103
|
+
# @raise [PayloadNotFoundError] If the payload isn't in the store.
|
|
100
104
|
def fetch(data)
|
|
101
105
|
raise ArgumentError.new("Not a storage reference") unless self.class.storage_ref?(data)
|
|
102
106
|
|
|
@@ -113,12 +117,12 @@ module PatientHttp
|
|
|
113
117
|
stored_data
|
|
114
118
|
end
|
|
115
119
|
|
|
116
|
-
#
|
|
120
|
+
# Deletes a stored payload.
|
|
117
121
|
#
|
|
118
|
-
# This method is idempotent
|
|
119
|
-
#
|
|
122
|
+
# This method is idempotent. You can call it with `nil`, with a hash that
|
|
123
|
+
# isn't a reference, or with a reference to a deleted payload.
|
|
120
124
|
#
|
|
121
|
-
# @param data [Hash, nil]
|
|
125
|
+
# @param data [Hash, nil] The reference. Other values are ignored.
|
|
122
126
|
# @return [void]
|
|
123
127
|
def delete(data)
|
|
124
128
|
return unless data && self.class.storage_ref?(data)
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# The error for a non-2xx response when the `raise_error_responses` option is
|
|
5
|
+
# set.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# The error holds the full {Response}, so you can read the status, headers,
|
|
8
|
+
# and body.
|
|
9
9
|
class HttpError < Error
|
|
10
|
-
# @return [Response] The HTTP response that
|
|
10
|
+
# @return [Response] The HTTP response that caused the error.
|
|
11
11
|
attr_reader :response
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
#
|
|
14
|
+
# Creates an error for a response. The status determines the class: a
|
|
15
|
+
# {ClientError} for 4xx, a {ServerError} for 5xx, and an `HttpError` for
|
|
16
|
+
# other non-2xx statuses.
|
|
15
17
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# @param response [Response] The HTTP response with non-2xx status code
|
|
20
|
-
# @return [HttpError, ClientError, ServerError] The appropriate error instance
|
|
18
|
+
# @param response [Response] The HTTP response with a non-2xx status.
|
|
19
|
+
# @return [HttpError, ClientError, ServerError] The error.
|
|
21
20
|
def new(response)
|
|
22
21
|
if response.client_error?
|
|
23
22
|
ClientError.allocate.tap { |error| error.send(:initialize, response) }
|
|
@@ -28,65 +27,72 @@ module PatientHttp
|
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
29
|
|
|
31
|
-
#
|
|
30
|
+
# Creates an error from its serialized form.
|
|
32
31
|
#
|
|
33
|
-
# @param hash [Hash] hash
|
|
34
|
-
# @return [HttpError]
|
|
32
|
+
# @param hash [Hash] The hash from {#as_json}.
|
|
33
|
+
# @return [HttpError] The error.
|
|
35
34
|
def load(hash)
|
|
36
35
|
response = Response.load(hash["response"])
|
|
37
36
|
new(response)
|
|
38
37
|
end
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
#
|
|
40
|
+
# Creates an error.
|
|
42
41
|
#
|
|
43
|
-
# @param response [Response] The HTTP response with non-2xx status
|
|
42
|
+
# @param response [Response] The HTTP response with a non-2xx status.
|
|
44
43
|
def initialize(response)
|
|
45
44
|
super("HTTP #{response.status} response from #{response.http_method.to_s.upcase} #{response.url}")
|
|
46
45
|
@response = response
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
#
|
|
48
|
+
# Returns the HTTP status of the response.
|
|
50
49
|
#
|
|
51
|
-
# @return [Integer] HTTP status code
|
|
50
|
+
# @return [Integer] The HTTP status code.
|
|
52
51
|
def status
|
|
53
52
|
@response.status
|
|
54
53
|
end
|
|
55
54
|
|
|
56
|
-
# Returns the error type
|
|
55
|
+
# Returns the error type.
|
|
57
56
|
#
|
|
58
|
-
# @return [Symbol]
|
|
57
|
+
# @return [Symbol] Always `:http_error`.
|
|
59
58
|
def error_type
|
|
60
59
|
:http_error
|
|
61
60
|
end
|
|
62
61
|
|
|
62
|
+
# @return [String] The request URL.
|
|
63
63
|
def url
|
|
64
64
|
response.url
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# @return [Symbol] The HTTP method.
|
|
67
68
|
def http_method
|
|
68
69
|
response.http_method
|
|
69
70
|
end
|
|
70
71
|
|
|
72
|
+
# @return [Float] The request duration in seconds.
|
|
71
73
|
def duration
|
|
72
74
|
response.duration
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
# @return [String] The unique request ID.
|
|
75
78
|
def request_id
|
|
76
79
|
response.request_id
|
|
77
80
|
end
|
|
78
81
|
|
|
82
|
+
# @return [Class] The class of this error.
|
|
79
83
|
def error_class
|
|
80
84
|
self.class
|
|
81
85
|
end
|
|
82
86
|
|
|
87
|
+
# @return [CallbackArgs] The callback arguments that were passed with the
|
|
88
|
+
# request.
|
|
83
89
|
def callback_args
|
|
84
90
|
response.callback_args
|
|
85
91
|
end
|
|
86
92
|
|
|
87
|
-
#
|
|
93
|
+
# Returns the error as a JSON-compatible hash.
|
|
88
94
|
#
|
|
89
|
-
# @return [Hash]
|
|
95
|
+
# @return [Hash] The serialized error.
|
|
90
96
|
def as_json
|
|
91
97
|
{
|
|
92
98
|
"response" => @response.as_json
|
|
@@ -94,13 +100,11 @@ module PatientHttp
|
|
|
94
100
|
end
|
|
95
101
|
end
|
|
96
102
|
|
|
97
|
-
#
|
|
98
|
-
# and the raise_error_responses option is enabled.
|
|
103
|
+
# The error for a 4xx response when the `raise_error_responses` option is set.
|
|
99
104
|
class ClientError < HttpError
|
|
100
105
|
end
|
|
101
106
|
|
|
102
|
-
#
|
|
103
|
-
# and the raise_error_responses option is enabled.
|
|
107
|
+
# The error for a 5xx response when the `raise_error_responses` option is set.
|
|
104
108
|
class ServerError < HttpError
|
|
105
109
|
end
|
|
106
110
|
end
|