Packages
ex_aws
2.4.2
2.7.0
2.6.1
2.6.0
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.2
2.0.1
2.0.0
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.1
1.0.0-beta3
1.0.0-beta2
1.0.0-beta1
1.0.0-beta0
0.5.0
0.4.19
0.4.18
0.4.17
0.4.15
0.4.14
0.4.13
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.2
0.1.1
0.1.0
0.0.5
0.0.4
0.0.3
AWS client for Elixir. Currently supports Dynamo, DynamoStreams, EC2, Firehose, Kinesis, KMS, Lambda, RRDS, Route53, S3, SES, SNS, SQS, STS and others.
Current section
Files
Jump to
Current section
Files
lib/ex_aws/request.ex
defmodule ExAws.Request do
@moduledoc false
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, service) 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, service) 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,
service: service,
request_body: req_body,
operation: extract_operation(full_headers)
}
:telemetry.span(telemetry_event, telemetry_metadata, fn ->
result =
config[:http_client].request(
method,
safe_url,
req_body,
full_headers,
Map.get(config, :http_opts, [])
)
stop_metadata =
case result do
{:ok, %{status_code: status} = resp} when status in 200..299 or status == 304 ->
%{result: :ok, response_body: Map.get(resp, :body)}
error ->
%{result: :error, error: extract_error(error)}
end
telemetry_metadata = Map.merge(telemetry_metadata, stop_metadata)
{result, telemetry_metadata}
end)
end
defp extract_operation(headers), do: Enum.find_value(headers, &match_operation/1)
defp match_operation({"x-amz-target", value}), do: value
defp match_operation({_key, _value}), do: nil
defp extract_error({:ok, %{body: body}}), do: body
defp extract_error({:ok, response}), do: response
defp extract_error({:error, error}), do: error
defp extract_error(error), do: error
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, err}) do
# Mark as unhandled, might intereset error_parsers.
{:error, {:aws_unhandled, type, message, err}}
end
# Clear unhandled mark, so Request.request() callers don't see it.
def default_aws_error({:error, {:aws_unhandled, type, message, _}}) do
{:error, {type, message}}
end
def default_aws_error(result) do
result
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