-
Notifications
You must be signed in to change notification settings - Fork 520
/
request.ex
188 lines (157 loc) · 5.58 KB
/
request.ex
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
defmodule ExAws.Request do
@moduledoc """
Makes requests to AWS.
"""
require Logger
@type http_status :: pos_integer
@type success_content :: %{body: binary, headers: [{binary, binary}]}
@type success_t :: {:ok, success_content}
@type error_t :: {:error, {:http_error, http_status, binary}}
@type response_t :: success_t | error_t
def request(http_method, url, data, headers, config, service) do
body =
case data do
[] -> "{}"
d when is_binary(d) -> d
_ -> config[:json_codec].encode!(data)
end
request_and_retry(http_method, url, service, config, headers, body, {:attempt, 1})
end
def request_and_retry(_method, _url, _service, _config, _headers, _req_body, {:error, reason}),
do: {:error, reason}
def request_and_retry(method, url, service, config, headers, req_body, {:attempt, attempt}) do
full_headers = ExAws.Auth.headers(method, url, service, config, headers, req_body)
with {:ok, full_headers} <- full_headers do
safe_url = ExAws.Request.Url.sanitize(url, service)
if config[:debug_requests] do
Logger.debug(
"ExAws: Request URL: #{inspect(safe_url)} HEADERS: #{inspect(full_headers)} BODY: #{
inspect(req_body)
} ATTEMPT: #{attempt}"
)
end
case do_request(config, method, safe_url, req_body, full_headers, attempt) do
{:ok, %{status_code: status} = resp} when status in 200..299 or status == 304 ->
{:ok, resp}
{:ok, %{status_code: status} = _resp} when status == 301 ->
Logger.warn("ExAws: Received redirect, did you specify the correct region?")
{:error, {:http_error, status, "redirected"}}
{:ok, %{status_code: status} = resp} when status in 400..499 ->
case client_error(resp, config[:json_codec]) do
{:retry, reason} ->
request_and_retry(
method,
url,
service,
config,
headers,
req_body,
attempt_again?(attempt, reason, config)
)
{:error, reason} ->
{:error, reason}
end
{:ok, %{status_code: status} = resp} when status >= 500 ->
body = Map.get(resp, :body)
reason = {:http_error, status, body}
request_and_retry(
method,
url,
service,
config,
headers,
req_body,
attempt_again?(attempt, reason, config)
)
{:error, %{reason: reason}} ->
Logger.warn(
"ExAws: HTTP ERROR: #{inspect(reason)} for URL: #{inspect(safe_url)} ATTEMPT: #{
attempt
}"
)
request_and_retry(
method,
url,
service,
config,
headers,
req_body,
attempt_again?(attempt, reason, config)
)
end
end
end
defp do_request(config, method, safe_url, req_body, full_headers, attempt) do
telemetry_event = Map.get(config, :telemetry_event, [:ex_aws, :request])
telemetry_options = Map.get(config, :telemetry_options, [])
telemetry_metadata = %{options: telemetry_options, attempt: attempt}
:telemetry.span(telemetry_event, telemetry_metadata, fn ->
result =
config[:http_client].request(
method,
safe_url,
req_body,
full_headers,
Map.get(config, :http_opts, [])
)
telemetry_result =
case result do
{:ok, %{status_code: status}} when status in 200..299 or status == 304 -> :ok
_ -> :error
end
telemetry_metadata = Map.put(telemetry_metadata, :result, telemetry_result)
{result, telemetry_metadata}
end)
end
def client_error(%{status_code: status, body: body} = error, json_codec) do
case json_codec.decode(body) do
{:ok, %{"__type" => error_type, "message" => message} = err} ->
handle_error(error_type, message, status, err)
# Rather irritatingly, as of 1.15, the local version of DynamoDB returns this with a
# capital M in "Message"
{:ok, %{"__type" => error_type, "Message" => message} = err} ->
handle_error(error_type, message, status, err)
_ ->
{:error, {:http_error, status, error}}
end
end
def client_error(%{status_code: status} = error, _) do
{:error, {:http_error, status, error}}
end
def handle_aws_error("ProvisionedThroughputExceededException" = type, message, _) do
{:retry, {type, message}}
end
def handle_aws_error("ThrottlingException" = type, message, _) do
{:retry, {type, message}}
end
def handle_aws_error(type, message, %{"expectedSequenceToken" => expected_sequence_token}) do
{:error, {type, message, expected_sequence_token}}
end
def handle_aws_error(type, message, _) do
{:error, {type, message}}
end
defp handle_error(error_type, message, status, err) do
error_type
|> String.split("#")
|> case do
[_, type] -> handle_aws_error(type, message, err)
[type] -> handle_aws_error(type, message, err)
_ -> {:error, {:http_error, status, err}}
end
end
def attempt_again?(attempt, reason, config) do
if attempt >= config[:retries][:max_attempts] do
{:error, reason}
else
attempt |> backoff(config)
{:attempt, attempt + 1}
end
end
def backoff(attempt, config) do
(config[:retries][:base_backoff_in_ms] * :math.pow(2, attempt))
|> min(config[:retries][:max_backoff_in_ms])
|> trunc
|> :rand.uniform()
|> :timer.sleep()
end
end