-
Notifications
You must be signed in to change notification settings - Fork 1
/
http.rb
220 lines (190 loc) · 6.66 KB
/
http.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# frozen_string_literal: true
require "net/http"
require "faraday"
require "faraday_middleware"
require_relative "../faraday/logfmt_logger"
require_relative "../faraday/url_size_limit"
require_relative "heroku"
require_relative "string_utilities"
module Twingly
module HTTP
class ConnectionError < StandardError; end
class UrlSizeLimitExceededError < StandardError; end
class RedirectLimitReachedError < StandardError; end
class Client # rubocop:disable Metrics/ClassLength
DEFAULT_RETRYABLE_EXCEPTIONS = [
Faraday::ConnectionFailed,
Faraday::SSLError,
Zlib::BufError,
Zlib::DataError,
].freeze
TIMEOUT_EXCEPTIONS = [
Faraday::TimeoutError,
Net::OpenTimeout,
].freeze
DEFAULT_HTTP_TIMEOUT = 20
DEFAULT_HTTP_OPEN_TIMEOUT = 10
DEFAULT_NUMBER_OF_RETRIES = 0
DEFAULT_RETRY_INTERVAL = 1
DEFAULT_MAX_URL_SIZE_BYTES = Float::INFINITY
DEFAULT_FOLLOW_REDIRECTS_LIMIT = 3
attr_writer :http_timeout
attr_writer :http_open_timeout
attr_writer :number_of_retries
attr_writer :retry_interval
attr_writer :on_retry_callback
attr_writer :max_url_size_bytes
attr_writer :request_id
attr_writer :follow_redirects
attr_accessor :follow_redirects_limit
attr_accessor :logger
attr_accessor :retryable_exceptions
def initialize(base_user_agent:, logger: default_logger)
@base_user_agent = base_user_agent
@logger = logger
initialize_defaults
end
def get(url, params: {}, headers: {})
http_response_for(:get, url: url, params: params, headers: headers)
end
def post(url, body:, headers: {})
http_response_for(:post, url: url, body: body, headers: headers)
end
private
def default_logger
Logger.new(File::NULL)
end
def initialize_defaults
@request_id = nil
@http_timeout = DEFAULT_HTTP_TIMEOUT
@http_open_timeout = DEFAULT_HTTP_OPEN_TIMEOUT
@retryable_exceptions = DEFAULT_RETRYABLE_EXCEPTIONS
@number_of_retries = DEFAULT_NUMBER_OF_RETRIES
@retry_interval = DEFAULT_RETRY_INTERVAL
@on_retry_callback = nil
@follow_redirects = false
@follow_redirects_limit = DEFAULT_FOLLOW_REDIRECTS_LIMIT
@max_url_size_bytes = DEFAULT_MAX_URL_SIZE_BYTES
end
# rubocop:disable Metrics/MethodLength
def http_response_for(method, **args)
response = case method
when :get
http_get_response(**args)
when :post
http_post_response(**args)
end
Response.new(headers: response.headers.to_h,
status: response.status,
body: response.body)
rescue *(@retryable_exceptions + TIMEOUT_EXCEPTIONS)
raise ConnectionError
rescue Faraday::UrlSizeLimit::LimitExceededError => error
raise UrlSizeLimitExceededError, error.message
rescue FaradayMiddleware::RedirectLimitReached => error
raise RedirectLimitReachedError, error.message
end
# rubocop:enable all
def http_get_response(url:, params:, headers:)
binary_url = url.dup.force_encoding(Encoding::BINARY)
http_client = create_http_client
headers = default_headers.merge(headers)
http_client.get do |request|
request.url(binary_url)
request.params.merge!(params)
request.headers.merge!(headers)
request.options.timeout = @http_timeout
request.options.open_timeout = @http_open_timeout
end
end
def http_post_response(url:, body:, headers:)
binary_url = url.dup.force_encoding(Encoding::BINARY)
http_client = create_http_client
headers = default_headers.merge(headers)
http_client.post do |request|
request.url(binary_url)
request.headers.merge!(headers)
request.body = body
request.options.timeout = @http_timeout
request.options.open_timeout = @http_open_timeout
end
end
def create_http_client # rubocop:disable Metrics/MethodLength
Faraday.new do |faraday|
faraday.request :url_size_limit,
max_size_bytes: @max_url_size_bytes
faraday.request :retry,
max: @number_of_retries,
interval: @retry_interval,
exceptions: @retryable_exceptions,
methods: [], # empty [] forces Faraday to run retry_if
retry_if: retry_if
faraday.response :logfmt_logger, @logger.dup,
headers: true,
bodies: true,
request_id: @request_id
if @follow_redirects
faraday.use FaradayMiddleware::FollowRedirects,
limit: @follow_redirects_limit
end
faraday.adapter Faraday.default_adapter
faraday.headers[:user_agent] = user_agent
end
end
def retry_if
lambda do |env, exception|
unwrapped_exception = unwrap_exception(exception)
# we do not retry on timeouts due to our request time budget
if timeout_error?(unwrapped_exception)
false
else
@on_retry_callback&.call(env, unwrapped_exception)
true
end
end
end
def unwrap_exception(exception)
if exception.respond_to?(:wrapped_exception)
exception.wrapped_exception
else
exception
end
end
def timeout_error?(error)
TIMEOUT_EXCEPTIONS.include?(error.class)
end
def user_agent
format(
"%<base>s (Release/%<release>s; Commit/%<commit>s)",
base: @base_user_agent,
release: Heroku.release_version,
commit: Heroku.slug_commit
)
end
def app_metadata
{
"dyno_id": Heroku.dyno_id,
"release": Heroku.release_version,
"git_head": Heroku.slug_commit,
}
end
def default_headers
{
"X-Request-Id": @request_id,
}.delete_if { |_name, value| value.to_s.strip.empty? }
end
end
class Response
attr_reader :headers
attr_reader :status
attr_reader :body
def initialize(headers: nil,
status: nil,
body: nil)
@headers = headers
@status = status
@body = body
end
end
end
end