-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
request.rb
181 lines (145 loc) · 4.9 KB
/
request.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
# frozen_string_literal: true
module Aitch
class Request
attr_accessor :request_method, :url, :data, :headers, :options, :redirects
CONTENT_TYPE = "Content-Type"
USER_AGENT = "User-Agent"
ACCEPT_ENCODING = "Accept-Encoding"
GZIP_DEFLATE = "gzip,deflate"
HTTPS = "https"
HEADER_SEPARATOR_RE = /[-_]/.freeze
JSON_RE = /\bjson\b/.freeze
alias params= data=
alias body= data=
def initialize(options)
self.headers = {}
self.options = {}
self.redirects = []
@_original_options = options.dup.freeze
options.each do |name, value|
public_send("#{name}=", value)
end
end
def perform
response = Response.new(options, client.request(request))
response.url = url
response = follow_redirect(response)
validate_response! response
response
rescue timeout_exception
raise RequestTimeoutError
end
def content_type=(content_type)
headers[CONTENT_TYPE] = content_type
end
def content_type
headers[CONTENT_TYPE] ||
options.fetch(:default_headers, {})[CONTENT_TYPE]
end
def request
@request ||= http_method_class.new(uri.request_uri).tap do |request|
set_body(request) if request.request_body_permitted?
set_user_agent(request)
set_gzip(request)
set_headers(request)
set_credentials(request)
end
end
def client
@client ||= Net::HTTP.new(uri.host, uri.port).tap do |client|
set_https(client)
set_timeout(client)
set_logger(client)
end
end
def uri
@uri ||= begin
normalized_url = if url.start_with?("/") && options[:base_url]
File.join(options[:base_url], url)
else
url
end
URI.new(normalized_url, data, http_method_class::REQUEST_HAS_BODY)
end
end
def http_method_class
Net::HTTP.const_get(request_method.to_s.capitalize)
rescue NameError
raise InvalidHTTPMethodError,
"unexpected HTTP verb: #{request_method.inspect}"
end
private def set_body(request)
body_data = data
body_data = data.to_h if data.respond_to?(:to_h) && !data.is_a?(Array)
if content_type.to_s.match?(JSON_RE)
body_data = ResponseParser::JSONParser.engine.dump(body_data)
end
request.body = if body_data.is_a?(Hash)
Utils.build_query(body_data)
else
body_data.to_s
end
end
private def set_headers(request)
all_headers = options.fetch(:default_headers, {}).merge(headers)
all_headers.each do |name, value|
value = value.call if value.respond_to?(:call)
name = name.to_s.split(HEADER_SEPARATOR_RE).map(&:capitalize).join("-")
request[name] = value.to_s
end
end
private def set_credentials(request)
return unless options[:user] || options[:password]
request.basic_auth(options[:user], options[:password])
end
private def set_https(client)
client.use_ssl = uri.scheme == HTTPS
client.verify_mode = OpenSSL::SSL::VERIFY_PEER
end
private def set_timeout(client)
client.read_timeout = options[:timeout]
end
private def set_logger(client)
logger = options[:logger]
client.set_debug_output(logger) if logger
end
private def set_user_agent(request)
request[USER_AGENT] = options[:user_agent]
end
private def set_gzip(request)
request[ACCEPT_ENCODING] = GZIP_DEFLATE
end
def timeout_exception
defined?(Net::ReadTimeout) ? Net::ReadTimeout : Timeout::Error
end
private def follow_redirect(response)
return response unless response.redirect?
redirect = Redirect.new(options)
redirected_from = [url]
while redirect.follow?(response)
location = Location.new(redirected_from, response.location).location
redirect.followed!
follow_request_method = response.code == 307 ? request_method : :get
follow_request_options = @_original_options.merge(
request_method: follow_request_method,
url: location
)
response = self.class.new(follow_request_options).perform
redirected_from += response.redirected_from
end
raise TooManyRedirectsError if redirect.enabled? && response.redirect?
response.redirected_from = redirected_from
response
end
private def validate_response!(response)
return unless options[:expect]
expected = [options[:expect]].flatten
return if expected.include?(response.code)
descriptions = expected
.map {|code| Response.description_for_code(code) }
.join(", ")
raise StatusCodeError,
"Expected(#{descriptions}) <=> Actual(#{response.description})"
end
end
end