butler-http 0.1.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 +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +22 -0
- data/README.md +659 -0
- data/Rakefile +10 -0
- data/lib/butler/async.rb +130 -0
- data/lib/butler/body.rb +130 -0
- data/lib/butler/client.rb +206 -0
- data/lib/butler/configuration.rb +161 -0
- data/lib/butler/connection.rb +21 -0
- data/lib/butler/connection_pool.rb +64 -0
- data/lib/butler/errors.rb +78 -0
- data/lib/butler/headers.rb +106 -0
- data/lib/butler/pipeline/chain.rb +18 -0
- data/lib/butler/pipeline/context.rb +21 -0
- data/lib/butler/pipeline/middleware.rb +16 -0
- data/lib/butler/pipeline/middlewares/circuit_breaker_middleware.rb +21 -0
- data/lib/butler/pipeline/middlewares/retry_middleware.rb +67 -0
- data/lib/butler/pipeline/middlewares/security_middleware.rb +14 -0
- data/lib/butler/pipeline/middlewares/telemetry_middleware.rb +37 -0
- data/lib/butler/pipeline/middlewares/timeout_middleware.rb +17 -0
- data/lib/butler/quic/crypto/aead.rb +69 -0
- data/lib/butler/quic/crypto/header_protection.rb +51 -0
- data/lib/butler/quic/crypto/hkdf.rb +44 -0
- data/lib/butler/quic/crypto/key_schedule.rb +50 -0
- data/lib/butler/quic/packet.rb +113 -0
- data/lib/butler/quic/varint.rb +56 -0
- data/lib/butler/rails/notifications.rb +10 -0
- data/lib/butler/rails/railtie.rb +24 -0
- data/lib/butler/request.rb +92 -0
- data/lib/butler/resilience/backoff.rb +17 -0
- data/lib/butler/resilience/circuit_breaker.rb +120 -0
- data/lib/butler/resilience/deadline.rb +55 -0
- data/lib/butler/resilience/retry_policy.rb +76 -0
- data/lib/butler/resilience/timeout.rb +22 -0
- data/lib/butler/response.rb +81 -0
- data/lib/butler/security/host_policy.rb +29 -0
- data/lib/butler/security/limits.rb +39 -0
- data/lib/butler/security/redirect_policy.rb +28 -0
- data/lib/butler/security/tls.rb +51 -0
- data/lib/butler/stream.rb +44 -0
- data/lib/butler/telemetry/instrumentation.rb +59 -0
- data/lib/butler/telemetry/logger.rb +37 -0
- data/lib/butler/telemetry/open_telemetry_bridge.rb +46 -0
- data/lib/butler/testing/fake_transport.rb +29 -0
- data/lib/butler/testing/stub.rb +68 -0
- data/lib/butler/testing/stub_registry.rb +32 -0
- data/lib/butler/testing.rb +33 -0
- data/lib/butler/transport.rb +118 -0
- data/lib/butler/uri.rb +77 -0
- data/lib/butler/version.rb +3 -0
- data/lib/butler.rb +176 -0
- data/sig/butler/client.rbs +41 -0
- data/sig/butler/configuration.rbs +63 -0
- data/sig/butler/errors.rbs +67 -0
- data/sig/butler/headers.rbs +22 -0
- data/sig/butler/request.rbs +21 -0
- data/sig/butler/response.rbs +28 -0
- data/sig/butler.rbs +22 -0
- metadata +191 -0
data/lib/butler/uri.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Wraps ::URI to add the handful of things Butler needs repeatedly: joining
|
|
2
|
+
# a client's base_url with a per-request path, merging a params: hash into
|
|
3
|
+
# the query string, and the origin/cross-origin comparisons the connection
|
|
4
|
+
# pool and redirect/credential-stripping logic depend on.
|
|
5
|
+
class Butler::URI
|
|
6
|
+
attr_reader :uri
|
|
7
|
+
|
|
8
|
+
def self.build(base_url, path = nil, params = nil)
|
|
9
|
+
resolved = resolve(base_url, path)
|
|
10
|
+
new(::URI.parse(resolved)).with_params(params)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.resolve(base_url, path)
|
|
14
|
+
if path.nil? || path.to_s.empty?
|
|
15
|
+
base_url.to_s
|
|
16
|
+
elsif path.to_s =~ %r{\Ahttps?://}i
|
|
17
|
+
path.to_s
|
|
18
|
+
elsif base_url
|
|
19
|
+
::URI.join(base_url.to_s, path.to_s).to_s
|
|
20
|
+
else
|
|
21
|
+
path.to_s
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
private_class_method :resolve
|
|
25
|
+
|
|
26
|
+
def initialize(uri)
|
|
27
|
+
@uri = uri
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def with_params(params)
|
|
31
|
+
return self if params.nil? || params.empty?
|
|
32
|
+
|
|
33
|
+
existing = uri.query ? ::URI.decode_www_form(uri.query) : []
|
|
34
|
+
new_uri = uri.dup
|
|
35
|
+
new_uri.query = ::URI.encode_www_form(existing + params.to_a)
|
|
36
|
+
self.class.new(new_uri)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def scheme
|
|
40
|
+
uri.scheme
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def host
|
|
44
|
+
uri.host
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def port
|
|
48
|
+
uri.port
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# "scheme://host:port" — the key the connection pool groups connections by.
|
|
52
|
+
def origin_key
|
|
53
|
+
"#{scheme}://#{host}:#{port}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The path+query Butler sends on the wire (never the full absolute URL).
|
|
57
|
+
def request_target
|
|
58
|
+
target = uri.path.to_s
|
|
59
|
+
target = "/" if target.empty?
|
|
60
|
+
target += "?#{uri.query}" if uri.query
|
|
61
|
+
target
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def cross_origin?(other)
|
|
65
|
+
origin_key != other.origin_key
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Resolves a redirect's Location header (absolute or relative) against
|
|
69
|
+
# this URI.
|
|
70
|
+
def merge(location)
|
|
71
|
+
self.class.new(uri.merge(location.to_s))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_s
|
|
75
|
+
uri.to_s
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/butler.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "logger"
|
|
5
|
+
require "weakref"
|
|
6
|
+
|
|
7
|
+
require_relative "butler/version"
|
|
8
|
+
require_relative "butler/errors"
|
|
9
|
+
require_relative "butler/headers"
|
|
10
|
+
require_relative "butler/uri"
|
|
11
|
+
require_relative "butler/body"
|
|
12
|
+
require_relative "butler/stream"
|
|
13
|
+
require_relative "butler/configuration"
|
|
14
|
+
require_relative "butler/request"
|
|
15
|
+
require_relative "butler/response"
|
|
16
|
+
|
|
17
|
+
require_relative "butler/telemetry/instrumentation"
|
|
18
|
+
require_relative "butler/telemetry/open_telemetry_bridge"
|
|
19
|
+
require_relative "butler/telemetry/logger"
|
|
20
|
+
|
|
21
|
+
require_relative "butler/security/tls"
|
|
22
|
+
require_relative "butler/security/host_policy"
|
|
23
|
+
require_relative "butler/security/redirect_policy"
|
|
24
|
+
require_relative "butler/security/limits"
|
|
25
|
+
|
|
26
|
+
require_relative "butler/resilience/deadline"
|
|
27
|
+
require_relative "butler/resilience/timeout"
|
|
28
|
+
require_relative "butler/resilience/backoff"
|
|
29
|
+
require_relative "butler/resilience/retry_policy"
|
|
30
|
+
require_relative "butler/resilience/circuit_breaker"
|
|
31
|
+
|
|
32
|
+
require_relative "butler/testing/stub"
|
|
33
|
+
require_relative "butler/testing/stub_registry"
|
|
34
|
+
require_relative "butler/testing/fake_transport"
|
|
35
|
+
require_relative "butler/testing"
|
|
36
|
+
|
|
37
|
+
require_relative "butler/connection"
|
|
38
|
+
require_relative "butler/transport"
|
|
39
|
+
require_relative "butler/connection_pool"
|
|
40
|
+
|
|
41
|
+
# QUIC/HTTP3 (see docs/architecture.md and the "hand-rolled HTTP/3 (QUIC)
|
|
42
|
+
# support" plan): foundational packet-protection code only so far, not yet
|
|
43
|
+
# reachable from Client/Transport.current or any config option — nothing
|
|
44
|
+
# above this line depends on it, and requiring butler still works exactly
|
|
45
|
+
# as before if you never touch Butler::Quic directly.
|
|
46
|
+
require_relative "butler/quic/varint"
|
|
47
|
+
require_relative "butler/quic/crypto/hkdf"
|
|
48
|
+
require_relative "butler/quic/crypto/key_schedule"
|
|
49
|
+
require_relative "butler/quic/crypto/aead"
|
|
50
|
+
require_relative "butler/quic/crypto/header_protection"
|
|
51
|
+
require_relative "butler/quic/packet"
|
|
52
|
+
|
|
53
|
+
require_relative "butler/pipeline/context"
|
|
54
|
+
require_relative "butler/pipeline/middleware"
|
|
55
|
+
require_relative "butler/pipeline/chain"
|
|
56
|
+
require_relative "butler/pipeline/middlewares/security_middleware"
|
|
57
|
+
require_relative "butler/pipeline/middlewares/telemetry_middleware"
|
|
58
|
+
require_relative "butler/pipeline/middlewares/timeout_middleware"
|
|
59
|
+
require_relative "butler/pipeline/middlewares/circuit_breaker_middleware"
|
|
60
|
+
require_relative "butler/pipeline/middlewares/retry_middleware"
|
|
61
|
+
|
|
62
|
+
require_relative "butler/async"
|
|
63
|
+
require_relative "butler/client"
|
|
64
|
+
|
|
65
|
+
# Butler is a Fiber-native HTTP client: HTTP/1.1 and HTTP/2 (transparent ALPN
|
|
66
|
+
# negotiation, multiplexed connection reuse via the `async`/`async-http`
|
|
67
|
+
# gems), structured concurrency, built-in resilience (timeouts, deadlines,
|
|
68
|
+
# retries with backoff+jitter, a circuit breaker), security-conscious
|
|
69
|
+
# defaults, a small middleware pipeline, standalone instrumentation with
|
|
70
|
+
# optional OpenTelemetry/Rails bridges, and native request stubbing for
|
|
71
|
+
# tests — none of which ever exposes its underlying transport in the public
|
|
72
|
+
# API. See docs/architecture.md for the full request lifecycle.
|
|
73
|
+
#
|
|
74
|
+
# Module-level calls (Butler.get/post/...) are the quickest way to reach
|
|
75
|
+
# for Butler — they run against Butler.default_client, one lazily-built,
|
|
76
|
+
# connection-pooled Butler::Client shared for the life of the process, so
|
|
77
|
+
# repeated calls still get everything a real Client gives you (pooling,
|
|
78
|
+
# retries, a circuit breaker) rather than reconnecting from scratch each
|
|
79
|
+
# time:
|
|
80
|
+
#
|
|
81
|
+
# response = Butler.get("https://api.example.com/users")
|
|
82
|
+
# response = Butler.post("https://api.example.com/users", json: { name: "Ram" })
|
|
83
|
+
#
|
|
84
|
+
# Reach for a real Butler::Client — everything above is built on it — once
|
|
85
|
+
# you want per-instance configuration (a fixed base_url, custom retry
|
|
86
|
+
# policy, middleware) rather than the shared default:
|
|
87
|
+
#
|
|
88
|
+
# client = Butler::Client.new(base_url: "https://api.example.com")
|
|
89
|
+
# response = client.get("/users")
|
|
90
|
+
# response.status; response.headers; response.body; response.json
|
|
91
|
+
#
|
|
92
|
+
# client.async do |tasks|
|
|
93
|
+
# users = tasks.async { client.get("/users") }
|
|
94
|
+
# orders = tasks.async { client.get("/orders") }
|
|
95
|
+
# { users: users.wait, orders: orders.wait }
|
|
96
|
+
# end
|
|
97
|
+
class Butler
|
|
98
|
+
class << self
|
|
99
|
+
def configuration
|
|
100
|
+
@configuration ||= Butler::Configuration.new
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def configure
|
|
104
|
+
yield configuration
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The Butler::Client every module-level call (Butler.get, Butler.post,
|
|
108
|
+
# ...) runs against — built lazily on first use and memoized for the
|
|
109
|
+
# life of the process, so it keeps its own connection pool/circuit
|
|
110
|
+
# breaker state across calls instead of reconnecting every time. It
|
|
111
|
+
# snapshots Butler.configuration as of whenever it's first built, the
|
|
112
|
+
# same way Butler::Client.new always has — call Butler.configure
|
|
113
|
+
# before your first module-level call (or Butler.reset_default_client!
|
|
114
|
+
# afterward) if you need later configuration changes to reach it.
|
|
115
|
+
def default_client
|
|
116
|
+
return @default_client if @default_client
|
|
117
|
+
|
|
118
|
+
@default_client_mutex ||= Mutex.new
|
|
119
|
+
@default_client_mutex.synchronize { @default_client ||= Butler::Client.new }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Discards the memoized default client (its connections are closed
|
|
123
|
+
# first) so the next module-level call rebuilds one from whatever
|
|
124
|
+
# Butler.configuration looks like now. Mostly useful in tests, or after
|
|
125
|
+
# reconfiguring Butler well after the first module-level call already
|
|
126
|
+
# built one.
|
|
127
|
+
def reset_default_client!
|
|
128
|
+
return unless @default_client_mutex
|
|
129
|
+
|
|
130
|
+
@default_client_mutex.synchronize do
|
|
131
|
+
@default_client&.reset_connections!
|
|
132
|
+
@default_client = nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Client::HTTP_METHODS.each do |http_method|
|
|
137
|
+
define_method(http_method) do |path = nil, **opts|
|
|
138
|
+
default_client.public_send(http_method, path, **opts)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def async(&block)
|
|
143
|
+
default_client.async(&block)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def register_client(client)
|
|
147
|
+
@clients_mutex ||= Mutex.new
|
|
148
|
+
@clients ||= []
|
|
149
|
+
@clients_mutex.synchronize { @clients << WeakRef.new(client) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Closes every tracked client's pooled connections and resets the
|
|
153
|
+
# shared background reactor (see Butler::Async.background). Called
|
|
154
|
+
# automatically post-fork by Butler::Rails::Railtie so a forked Puma
|
|
155
|
+
# worker never inherits a parent's live Async::HTTP::Client/reactor
|
|
156
|
+
# state — Ruby Thread objects don't survive fork(), only the forking
|
|
157
|
+
# thread does — safe to call manually otherwise (e.g. after a known
|
|
158
|
+
# upstream restart).
|
|
159
|
+
def reset_connections!
|
|
160
|
+
Butler::Async.background.reset!
|
|
161
|
+
|
|
162
|
+
return unless @clients
|
|
163
|
+
|
|
164
|
+
@clients_mutex.synchronize do
|
|
165
|
+
@clients.each do |ref|
|
|
166
|
+
ref.__getobj__.reset_connections! if ref.weakref_alive?
|
|
167
|
+
rescue WeakRef::RefError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
@clients.reject! { |ref| !ref.weakref_alive? }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
require "butler/rails/railtie" if defined?(::Rails::Railtie)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
class Client
|
|
3
|
+
HTTP_METHODS: Array[Symbol]
|
|
4
|
+
|
|
5
|
+
@configuration: Butler::Configuration
|
|
6
|
+
@connection_pool: Butler::ConnectionPool
|
|
7
|
+
@circuit_breaker: Butler::Resilience::CircuitBreaker
|
|
8
|
+
@retry_policy: Butler::Resilience::RetryPolicy
|
|
9
|
+
@custom_middlewares: Array[untyped]
|
|
10
|
+
@pipeline: Butler::Pipeline::Chain
|
|
11
|
+
|
|
12
|
+
attr_reader configuration: Butler::Configuration
|
|
13
|
+
|
|
14
|
+
def initialize: (**untyped options) -> void
|
|
15
|
+
|
|
16
|
+
def get: (?String? path, **untyped opts) -> Butler::Response
|
|
17
|
+
def post: (?String? path, **untyped opts) -> Butler::Response
|
|
18
|
+
def put: (?String? path, **untyped opts) -> Butler::Response
|
|
19
|
+
def patch: (?String? path, **untyped opts) -> Butler::Response
|
|
20
|
+
def delete: (?String? path, **untyped opts) -> Butler::Response
|
|
21
|
+
def head: (?String? path, **untyped opts) -> Butler::Response
|
|
22
|
+
def options: (?String? path, **untyped opts) -> Butler::Response
|
|
23
|
+
|
|
24
|
+
def use: (untyped middleware) -> self
|
|
25
|
+
|
|
26
|
+
def request: (String | Symbol method, ?String? path, **untyped opts) -> Butler::Response
|
|
27
|
+
|
|
28
|
+
def async: () { (Butler::Async::TaskGroup) -> untyped } -> untyped
|
|
29
|
+
|
|
30
|
+
def reset_connections!: () -> void
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def perform_request: (String | Symbol method, String? path, Hash[Symbol, untyped] opts) -> Butler::Response
|
|
35
|
+
def per_call_config: (Hash[Symbol, untyped] opts) -> Butler::Configuration
|
|
36
|
+
def maybe_raise: (Butler::Response response) -> Butler::Response
|
|
37
|
+
def strip_credential_headers: (untyped headers) -> Butler::Headers
|
|
38
|
+
def build_pipeline: () -> Butler::Pipeline::Chain
|
|
39
|
+
def attempt_timeout: (Butler::Configuration config, Butler::Resilience::Deadline deadline) -> Numeric
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
class Configuration
|
|
3
|
+
class RetryOptions
|
|
4
|
+
attr_accessor max_attempts: Integer
|
|
5
|
+
attr_accessor retryable_status_codes: Array[Integer]
|
|
6
|
+
attr_accessor backoff: Symbol
|
|
7
|
+
attr_accessor base_delay: Float
|
|
8
|
+
attr_accessor max_delay: Float
|
|
9
|
+
attr_accessor jitter: bool
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CircuitBreakerOptions
|
|
13
|
+
attr_accessor enabled: bool
|
|
14
|
+
attr_accessor scope: Symbol
|
|
15
|
+
attr_accessor failure_threshold: Integer
|
|
16
|
+
attr_accessor recovery_timeout: Integer
|
|
17
|
+
attr_accessor max_tracked_hosts: Integer
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class SecurityOptions
|
|
21
|
+
attr_accessor verify_tls: bool
|
|
22
|
+
attr_accessor allowed_hosts: Array[String]?
|
|
23
|
+
attr_accessor blocked_hosts: Array[String]
|
|
24
|
+
attr_accessor max_response_size: Integer
|
|
25
|
+
attr_accessor max_header_size: Integer
|
|
26
|
+
attr_accessor strip_credentials_on_redirect: bool
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class PoolOptions
|
|
30
|
+
attr_accessor max_connections: Integer
|
|
31
|
+
attr_accessor idle_timeout: Integer
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class TelemetryOptions
|
|
35
|
+
attr_accessor enabled: bool
|
|
36
|
+
attr_accessor opentelemetry: Symbol | bool
|
|
37
|
+
attr_accessor logger: untyped
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_accessor base_url: String?
|
|
41
|
+
attr_accessor user_agent: String?
|
|
42
|
+
attr_accessor connect_timeout: Numeric
|
|
43
|
+
attr_accessor read_timeout: Numeric
|
|
44
|
+
attr_accessor write_timeout: Numeric
|
|
45
|
+
attr_accessor request_timeout: Numeric?
|
|
46
|
+
attr_accessor deadline: Numeric?
|
|
47
|
+
attr_accessor follow_redirects: bool
|
|
48
|
+
attr_accessor max_redirects: Integer
|
|
49
|
+
attr_accessor http_version: Symbol
|
|
50
|
+
attr_accessor proxy: String?
|
|
51
|
+
attr_accessor raise_on_error: bool
|
|
52
|
+
|
|
53
|
+
attr_reader default_headers: Butler::Headers
|
|
54
|
+
attr_reader retry: RetryOptions
|
|
55
|
+
attr_reader circuit_breaker: CircuitBreakerOptions
|
|
56
|
+
attr_reader security: SecurityOptions
|
|
57
|
+
attr_reader pool: PoolOptions
|
|
58
|
+
attr_reader telemetry: TelemetryOptions
|
|
59
|
+
|
|
60
|
+
def initialize: () -> void
|
|
61
|
+
def apply: (Hash[Symbol, untyped] options) -> self
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
module Errors
|
|
3
|
+
class Error < StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class RequestError < Error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class TooManyRedirectsError < RequestError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class HostNotAllowed < RequestError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class TransportError < Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ConnectionError < TransportError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class TimeoutError < TransportError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class TLSError < TransportError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class CertificateVerificationError < TLSError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class DNSFailure < TransportError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ProtocolError < TransportError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class HTTPError < Error
|
|
40
|
+
attr_reader response: Butler::Response
|
|
41
|
+
|
|
42
|
+
def initialize: (Butler::Response response) -> void
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class ClientError < HTTPError
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class ServerError < HTTPError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class RetryExhausted < Error
|
|
52
|
+
attr_reader cause_response: Butler::Response?
|
|
53
|
+
attr_reader attempts: Integer?
|
|
54
|
+
|
|
55
|
+
def initialize: (String message, ?cause_response: Butler::Response?, ?attempts: Integer?) -> void
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class CircuitOpen < Error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Cancelled < Error
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class LimitExceeded < Error
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
class Headers
|
|
3
|
+
include Enumerable[[String, String]]
|
|
4
|
+
|
|
5
|
+
def initialize: (?untyped pairs) -> void
|
|
6
|
+
def self.from_a: (Array[[String, String]] pairs) -> Butler::Headers
|
|
7
|
+
def self.coerce: (untyped headers) -> Butler::Headers
|
|
8
|
+
|
|
9
|
+
def []: (String | Symbol key) -> String?
|
|
10
|
+
def []=: (String | Symbol key, untyped value) -> untyped
|
|
11
|
+
def add: (String | Symbol key, untyped value) -> untyped
|
|
12
|
+
def key?: (String | Symbol key) -> bool
|
|
13
|
+
def delete: (String | Symbol key) -> String?
|
|
14
|
+
def values_at: (String | Symbol key) -> Array[String]
|
|
15
|
+
def each: () { (String, String) -> void } -> self
|
|
16
|
+
def to_a: () -> Array[[String, String]]
|
|
17
|
+
def to_h: () -> Hash[String, String]
|
|
18
|
+
def merge: (untyped other) -> Butler::Headers
|
|
19
|
+
def empty?: () -> bool
|
|
20
|
+
def size: () -> Integer
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
class Request
|
|
3
|
+
attr_reader method: String
|
|
4
|
+
attr_reader uri: Butler::URI
|
|
5
|
+
attr_reader headers: Butler::Headers
|
|
6
|
+
attr_reader body: untyped
|
|
7
|
+
attr_reader idempotent: bool
|
|
8
|
+
attr_reader basic_auth: untyped
|
|
9
|
+
|
|
10
|
+
def self.build: (method: String | Symbol, uri: Butler::URI, options: Hash[Symbol, untyped], config: Butler::Configuration) -> Butler::Request
|
|
11
|
+
|
|
12
|
+
def initialize: (method: String, uri: Butler::URI, headers: Butler::Headers, ?body: untyped, ?idempotent: bool, ?basic_auth: untyped, ?stream: bool) -> void
|
|
13
|
+
|
|
14
|
+
def stream?: () -> bool
|
|
15
|
+
def origin_key: () -> String
|
|
16
|
+
def safe?: () -> bool
|
|
17
|
+
def retry_eligible?: () -> bool
|
|
18
|
+
def with_uri: (Butler::URI new_uri) -> Butler::Request
|
|
19
|
+
def without_credentials: () -> Butler::Request
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
class Response
|
|
3
|
+
attr_reader uri: Butler::URI
|
|
4
|
+
attr_reader status: Integer
|
|
5
|
+
attr_reader headers: Butler::Headers
|
|
6
|
+
|
|
7
|
+
def initialize: (uri: Butler::URI, status: Integer, headers: Butler::Headers, body: untyped) -> void
|
|
8
|
+
|
|
9
|
+
def body: () -> String
|
|
10
|
+
def stream: () -> untyped
|
|
11
|
+
def code: () -> Integer
|
|
12
|
+
|
|
13
|
+
def informational?: () -> bool
|
|
14
|
+
def success?: () -> bool
|
|
15
|
+
def ok?: () -> bool
|
|
16
|
+
def redirect?: () -> bool
|
|
17
|
+
def client_error?: () -> bool
|
|
18
|
+
def server_error?: () -> bool
|
|
19
|
+
def error?: () -> bool
|
|
20
|
+
|
|
21
|
+
def content_type: () -> String
|
|
22
|
+
def json?: () -> bool
|
|
23
|
+
def json: () -> untyped
|
|
24
|
+
def parsed: () -> untyped
|
|
25
|
+
|
|
26
|
+
def to_s: () -> String
|
|
27
|
+
end
|
|
28
|
+
end
|
data/sig/butler.rbs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
def self.configuration: () -> Butler::Configuration
|
|
5
|
+
def self.configure: () { (Butler::Configuration) -> void } -> void
|
|
6
|
+
|
|
7
|
+
def self.default_client: () -> Butler::Client
|
|
8
|
+
def self.reset_default_client!: () -> void
|
|
9
|
+
|
|
10
|
+
def self.get: (?String? path, **untyped opts) -> Butler::Response
|
|
11
|
+
def self.post: (?String? path, **untyped opts) -> Butler::Response
|
|
12
|
+
def self.put: (?String? path, **untyped opts) -> Butler::Response
|
|
13
|
+
def self.patch: (?String? path, **untyped opts) -> Butler::Response
|
|
14
|
+
def self.delete: (?String? path, **untyped opts) -> Butler::Response
|
|
15
|
+
def self.head: (?String? path, **untyped opts) -> Butler::Response
|
|
16
|
+
def self.options: (?String? path, **untyped opts) -> Butler::Response
|
|
17
|
+
|
|
18
|
+
def self.async: () { (Butler::Async::TaskGroup) -> untyped } -> untyped
|
|
19
|
+
|
|
20
|
+
def self.register_client: (Butler::Client client) -> void
|
|
21
|
+
def self.reset_connections!: () -> void
|
|
22
|
+
end
|