Skip to content

Commit

Permalink
Try #384:
Browse files Browse the repository at this point in the history
  • Loading branch information
bors[bot] authored Sep 3, 2021
2 parents e8776ce + b96b5a9 commit aa7769e
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 315 deletions.
28 changes: 15 additions & 13 deletions src/AWS.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module AWS

using Compat: Compat, @something
using Base: BufferStream
using Base64
using Compat: Compat, @something
using Dates
using Downloads: Downloads, Downloader, Curl
using HTTP
Expand Down Expand Up @@ -32,6 +33,7 @@ include("AWSConfig.jl")
include("AWSMetadata.jl")

include(joinpath("utilities", "request.jl"))
include(joinpath("utilities", "response.jl"))
include(joinpath("utilities", "sign.jl"))
include(joinpath("utilities", "downloads_backend.jl"))

Expand Down Expand Up @@ -200,15 +202,15 @@ Perform a RestXML request to AWS.
- `aws::AbstractAWSConfig`: AWSConfig containing credentials and other information for fulfilling the request, default value is the global configuration
# Returns
- `Tuple or Dict`: If `return_headers` is passed in through `args` a Tuple containing the Headers and Response will be returned, otherwise just a Dict
- `AWS.Response`: A struct containing the response details
"""
function (service::RestXMLService)(
request_method::String,
request_uri::String,
args::AbstractDict{String,<:Any}=Dict{String,Any}();
aws_config::AbstractAWSConfig=global_aws_config(),
)
return_headers = _pop!(args, "return_headers", false)
_delete_outdated_kw_args!(args)

request = Request(;
_extract_common_kw_args(service, args)...,
Expand All @@ -234,7 +236,7 @@ function (service::RestXMLService)(
aws_config, service.endpoint_prefix, request.resource
)

return submit_request(aws_config, request; return_headers=return_headers)
return submit_request(aws_config, request)
end

"""
Expand All @@ -253,15 +255,15 @@ Perform a Query request to AWS.
- `aws::AbstractAWSConfig`: AWSConfig containing credentials and other information for fulfilling the request, default value is the global configuration
# Returns
- `Tuple or Dict`: If `return_headers` is passed in through `args` a Tuple containing the Headers and Response will be returned, otherwise just a Dict
- `AWS.Response`: A struct containing the response details
"""
function (service::QueryService)(
operation::String,
args::AbstractDict{String,<:Any}=Dict{String,Any}();
aws_config::AbstractAWSConfig=global_aws_config(),
)
POST_RESOURCE = "/"
return_headers = _pop!(args, "return_headers", false)
_delete_outdated_kw_args!(args)

request = Request(;
_extract_common_kw_args(service, args)...,
Expand All @@ -276,7 +278,7 @@ function (service::QueryService)(
args["Version"] = service.api_version
request.content = HTTP.escapeuri(_flatten_query(service.signing_name, args))

return submit_request(aws_config, request; return_headers=return_headers)
return submit_request(aws_config, request)
end

"""
Expand All @@ -295,15 +297,15 @@ Perform a JSON request to AWS.
- `aws::AbstractAWSConfig`: AWSConfig containing credentials and other information for fulfilling the request, default value is the global configuration
# Returns
- `Tuple or Dict`: If `return_headers` is passed in through `args` a Tuple containing the Headers and Response will be returned, otherwise just a Dict
- `AWS.Response`: A struct containing the response details
"""
function (service::JSONService)(
operation::String,
args::AbstractDict{String,<:Any}=Dict{String,Any}();
aws_config::AbstractAWSConfig=global_aws_config(),
)
POST_RESOURCE = "/"
return_headers = _pop!(args, "return_headers", false)
_delete_outdated_kw_args!(args)

request = Request(;
_extract_common_kw_args(service, args)...,
Expand All @@ -316,7 +318,7 @@ function (service::JSONService)(
request.headers["Content-Type"] = "application/x-amz-json-$(service.json_version)"
request.headers["X-Amz-Target"] = "$(service.target).$(operation)"

return submit_request(aws_config, request; return_headers=return_headers)
return submit_request(aws_config, request)
end

"""
Expand All @@ -336,15 +338,15 @@ Perform a RestJSON request to AWS.
- `aws::AbstractAWSConfig`: AWSConfig containing credentials and other information for fulfilling the request, default value is the global configuration
# Returns
- `Tuple or Dict`: If `return_headers` is passed in through `args` a Tuple containing the Headers and Response will be returned, otherwise just a Dict
- `AWS.Response`: A struct containing the response details
"""
function (service::RestJSONService)(
request_method::String,
request_uri::String,
args::AbstractDict{String,<:Any}=Dict{String,String}();
aws_config::AbstractAWSConfig=global_aws_config(),
)
return_headers = _pop!(args, "return_headers", false)
_delete_outdated_kw_args!(args)

request = Request(;
_extract_common_kw_args(service, args)...,
Expand All @@ -363,7 +365,7 @@ function (service::RestJSONService)(
request.headers["Content-Type"] = "application/json"
request.content = json(args)

return submit_request(aws_config, request; return_headers=return_headers)
return submit_request(aws_config, request)
end

function __init__()
Expand Down
4 changes: 2 additions & 2 deletions src/AWSCredentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ function dot_aws_credentials(profile=nothing)
credential_file = @mock dot_aws_credentials_file()

if isfile(credential_file)
ini = read(Inifile(), credential_file)
ini = Base.read(Inifile(), credential_file)
p = @something profile _aws_get_profile()
access_key, secret_key, token = _aws_get_credential_details(p, ini)

Expand Down Expand Up @@ -404,7 +404,7 @@ function dot_aws_config(profile=nothing)
config_file = @mock dot_aws_config_file()

if isfile(config_file)
ini = read(Inifile(), config_file)
ini = Base.read(Inifile(), config_file)
p = @something profile _aws_get_profile()
access_key, secret_key, token = _aws_get_credential_details(p, ini)

Expand Down
40 changes: 32 additions & 8 deletions src/AWSExceptions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ using JSON
using XMLDict
using XMLDict: XMLDictElement

const BODY_STREAMED_PLACEHOLDER = b"[Message Body was streamed]"

export AWSException, ProtocolNotDefined, InvalidFileName, NoCredentials

struct ProtocolNotDefined <: Exception
Expand All @@ -27,18 +29,38 @@ struct AWSException <: Exception
message::String
info::Union{XMLDictElement,Dict,String,Nothing}
cause::HTTP.StatusError
streamed_body::Union{String,Nothing}
end

function Base.show(io::IO, e::AWSException)
message = isempty(e.message) ? "" : (" -- " * e.message)
return println(io, "AWSException: ", string(e.code, message, "\n", e.cause))
print(io, AWSException, ": ", e.code)
!isempty(e.message) && print(io, " -- ", e.message)
print(io, "\n\n", e.cause)

if e.streamed_body !== nothing
print(io, "\n\n")
if isempty(e.streamed_body)
printstyled(io, "(empty body)"; bold=true)
else
print(io, e.streamed_body)
end
end

println(io)
return nothing
end

http_message(e::HTTP.StatusError) = String(copy(e.response.body))
http_status(e::HTTP.StatusError) = e.status
content_type(e::HTTP.StatusError) = HTTP.header(e.response, "Content-Type")
is_valid_xml_string(str) = startswith(str, '<')

function AWSException(e::HTTP.StatusError)
AWSException(e::HTTP.StatusError) = AWSException(e, String(copy(e.response.body)))

function AWSException(e::HTTP.StatusError, body::IO)
return AWSException(e, String(take!(body)))
end

function AWSException(e::HTTP.StatusError, body::AbstractString)
code = string(http_status(e))
message = "AWSException"
info = Dict{String,Dict}()
Expand All @@ -50,16 +72,16 @@ function AWSException(e::HTTP.StatusError)

# Extract API error code from JSON error message...
if occursin(r"^application/x-amz-json-1\.[01]$", content_type(e))
info = JSON.parse(http_message(e))
info = JSON.parse(body)
if haskey(info, "__type")
code = rsplit(info["__type"], '#'; limit=2)[end]
end
end

# Extract API error code from XML error message...
error_content_types = ["", "application/xml", "text/xml"]
if content_type(e) in error_content_types && is_valid_xml_string(http_message(e))
info = parse_xml(http_message(e))
if content_type(e) in error_content_types && is_valid_xml_string(body)
info = parse_xml(body)
end

# There are times when Errors or Error are returned back
Expand All @@ -72,7 +94,9 @@ function AWSException(e::HTTP.StatusError)
message = get(info, "Message", message)
message = get(info, "message", message)

return AWSException(code, message, info, e)
streamed_body = e.response.body == BODY_STREAMED_PLACEHOLDER ? body : nothing

return AWSException(code, message, info, e, streamed_body)
end

end
6 changes: 3 additions & 3 deletions src/utilities/credentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ function _can_read_file(file_name::String)
false
end
end
_begins_with_ec2(file_name::String) = return uppercase(String(read(file_name, 3))) == "EC2"
_begins_with_ec2(file_name::String) = uppercase(String(Base.read(file_name, 3))) == "EC2"
function _ends_with_ec2(file_name::String)
return endswith(strip(uppercase(read(file_name, String))), "EC2")
return endswith(strip(uppercase(Base.read(file_name, String))), "EC2")
end

"""
Expand Down Expand Up @@ -49,7 +49,7 @@ end

function _aws_profile_config(config_file::AbstractString, profile)
isfile(config_file) || return Dict()
return _aws_profile_config(read(Inifile(), config_file), profile)
return _aws_profile_config(Base.read(Inifile(), config_file), profile)
end

function _aws_profile_config(config_file::Nothing, profile)
Expand Down
18 changes: 6 additions & 12 deletions src/utilities/downloads_backend.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ body_length(x::AbstractString) = sizeof(x)
read_body(x::IOBuffer) = take!(x)
function read_body(x::IO)
close(x)
return read(x)
return Base.read(x)
end

function _http_request(backend::DownloadsBackend, request)
function _http_request(backend::DownloadsBackend, request::Request, response_stream::IO)
# If we pass `output`, older versions of Downloads.jl will
# expect a message body in the response. Specifically, it sets
# <https://curl.se/libcurl/c/CURLOPT_NOBODY.html>
Expand All @@ -55,23 +55,16 @@ function _http_request(backend::DownloadsBackend, request)
# Therefore, we do not pass an `output` when the `request_method` is `HEAD`.
# (Note: this is fixed on the latest Downloads.jl, but we include this workaround
# for compatability).
if request.response_stream === nothing
request.response_stream = IOBuffer()
end
output_arg = if request.request_method == "HEAD"
NamedTuple()
else
(; output=request.response_stream)
(; output=response_stream)
end

# If we're going to return the stream, we don't want to read the body into an
# HTTP.Response we're never going to use. If we do that, the returned stream
# will have no data available (and reading from it could hang forever).
body_arg = if request.request_method == "HEAD" || request.return_stream
() -> NamedTuple()
else
() -> (; body=read_body(request.response_stream))
end
body_arg = () -> NamedTuple()

# HTTP.jl sets this header automatically.
request.headers["Content-Length"] = string(body_length(request.content))
Expand Down Expand Up @@ -122,7 +115,8 @@ function _http_request(backend::DownloadsBackend, request)
),
)
end
return http_response

return AWS.Response(http_response, response_stream)
catch e
@delay_retry if (
(isa(e, HTTP.StatusError) && AWS._http_status(e) >= 500) ||
Expand Down
Loading

0 comments on commit aa7769e

Please sign in to comment.