-
Notifications
You must be signed in to change notification settings - Fork 481
/
Copy pathhttp_client.rb
146 lines (112 loc) · 5.09 KB
/
http_client.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
# typed: strict
# frozen_string_literal: true
module ShopifyAPI
module Clients
class HttpClient
extend T::Sig
RETRY_WAIT_TIME = 1
sig { params(base_path: String, session: T.nilable(Auth::Session)).void }
def initialize(base_path:, session: nil)
session ||= Context.active_session
raise Errors::NoActiveSessionError, "No passed or active session" unless session
api_host = Context.api_host
@base_uri = T.let("https://#{api_host || session.shop}", String)
@base_uri_and_path = T.let("#{@base_uri}#{base_path}", String)
user_agent_prefix = Context.user_agent_prefix.nil? ? "" : "#{Context.user_agent_prefix} | "
@headers = T.let({
"User-Agent": "#{user_agent_prefix}Shopify API Library v#{VERSION} | Ruby #{RUBY_VERSION}",
"Accept": "application/json",
}, T::Hash[T.any(Symbol, String), T.untyped])
@headers["Host"] = session.shop unless api_host.nil?
unless session.access_token.nil? || T.must(session.access_token).empty?
@headers["X-Shopify-Access-Token"] = T.cast(session.access_token, String)
end
end
sig { params(request: HttpRequest, response_as_struct: T::Boolean).returns(HttpResponse) }
def request(request, response_as_struct: false)
request.verify
headers = @headers
headers["Content-Type"] = T.must(request.body_type) if request.body_type
headers = headers.merge(T.must(request.extra_headers)) if request.extra_headers
parsed_uri = URI(request_url(request))
headers = append_first_party_development_headers(headers, parsed_uri)
tries = 0
response = HttpResponse.new(code: 0, headers: {}, body: "")
while tries < request.tries
tries += 1
res = T.cast(HTTParty.send(
request.http_method,
parsed_uri.to_s,
headers: headers,
query: request.query,
body: request.body.class == Hash ? T.unsafe(request.body).to_json : request.body,
), HTTParty::Response)
begin
body = res.body.nil? || res.body.empty? ? {} : JSON.parse(res.body)
rescue JSON::ParserError
raise if res.code.to_i < 500
body = res.body
end
if response_as_struct && body.is_a?(Hash)
json_body = body.to_json
body = JSON.parse(json_body, object_class: OpenStruct)
end
response = HttpResponse.new(code: res.code.to_i, headers: res.headers.to_h, body: body)
if response.headers["x-shopify-api-deprecated-reason"]
reason = T.must(response.headers["x-shopify-api-deprecated-reason"])[0]
Context.logger.warn("Deprecated request to Shopify API at #{request.path}, received reason: #{reason}")
end
break if response.ok?
error_message = serialized_error(response)
unless [429, 500].include?(response.code)
raise ShopifyAPI::Errors::HttpResponseError.new(response: response), error_message
end
if tries == request.tries
raise ShopifyAPI::Errors::HttpResponseError.new(response: response), error_message if request.tries == 1
raise ShopifyAPI::Errors::MaxHttpRetriesExceededError.new(response: response),
"Exceeded maximum retry count of #{request.tries}. Last message: #{error_message}"
end
if response.code == 500 || response.headers["retry-after"].nil?
sleep(RETRY_WAIT_TIME)
else
sleep(T.must(response.headers["retry-after"])[0].to_i)
end
end
response
end
protected
sig { params(request: HttpRequest).returns(String) }
def request_url(request)
"#{@base_uri_and_path}/#{request.path}"
end
sig { params(response: HttpResponse).returns(String) }
def serialized_error(response)
body = {}
body["errors"] = response.body["errors"] if response.body["errors"]
if response.headers["x-request-id"]
id = T.must(response.headers["x-request-id"])[0]
body["error_reference"] = "If you report this error, please include this id: #{id}."
end
body.to_json
end
private
sig do
params(
headers: T::Hash[T.any(Symbol, String), T.untyped],
parsed_uri: URI::Generic,
).returns(T::Hash[T.any(Symbol, String), T.untyped])
end
def append_first_party_development_headers(headers, parsed_uri)
return headers unless defined?(DevServer)
return headers unless headers["Host"]&.include?(".my.shop.dev") || parsed_uri.host&.include?(".my.shop.dev")
return headers if headers["x-forwarded-host"]&.include?(".my.shop.dev")
# These headers are only used for first party applications in development mode
headers["x-forwarded-host"] = headers["Host"] || parsed_uri.host
headers["Host"] = T.unsafe(
Object.const_get("DevServer::Core"), # rubocop:disable Sorbet/ConstantsFromStrings
).new.host!(:app)
headers
end
end
end
end