Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge request and response types #569

Merged
merged 9 commits into from
Jul 28, 2023
Merged
151 changes: 82 additions & 69 deletions Sources/SotoCore/AWSClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
do {
try await shutdown()
} catch {
errorStorage.withLockedValue { errorStorage in

Check warning on line 124 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L124

Added line #L124 was not covered by tests
errorStorage = error
}
}
Expand Down Expand Up @@ -204,7 +204,7 @@
/// the HTTP client if it was created by the `AWSClient`.
public func shutdown() async throws {
guard self.isShutdown.compareExchange(expected: false, desired: true, ordering: .relaxed).exchanged else {
throw ClientError.alreadyShutdown

Check warning on line 207 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L207

Added line #L207 was not covered by tests
}
// shutdown credential provider ignoring any errors as credential provider that doesn't initialize
// can cause the shutdown process to fail
Expand All @@ -215,10 +215,10 @@
do {
try await self.httpClient.shutdown()
} catch {
self.clientLogger.log(level: self.options.errorLogLevel, "Error shutting down HTTP client", metadata: [
"aws-error": "\(error)",
])
throw error

Check warning on line 221 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L218-L221

Added lines #L218 - L221 were not covered by tests
}

case .shared:
Expand Down Expand Up @@ -247,18 +247,22 @@
return try await self.execute(
operation: operationName,
createRequest: {
try AWSRequest(
try AWSHTTPRequest(
operation: operationName,
path: path,
httpMethod: httpMethod,
method: httpMethod,
input: input,
hostPrefix: hostPrefix,
configuration: serviceConfig
)
},
processResponse: { response in
// flush response body contents to complete response read
for try await _ in response.body {}
return try await self.processEmptyResponse(
operation: operationName,
response: response,
serviceConfig: serviceConfig,
logger: logger
)
},
config: serviceConfig,
logger: logger
Expand All @@ -282,16 +286,20 @@
return try await self.execute(
operation: operationName,
createRequest: {
try AWSRequest(
try AWSHTTPRequest(
operation: operationName,
path: path,
httpMethod: httpMethod,
method: httpMethod,
configuration: serviceConfig
)
},
processResponse: { response in
// flush response body contents to complete response read
for try await _ in response.body {}
return try await self.processEmptyResponse(
operation: operationName,
response: response,
serviceConfig: serviceConfig,
logger: logger
)
},
config: serviceConfig,
logger: logger
Expand All @@ -317,15 +325,20 @@
return try await self.execute(
operation: operationName,
createRequest: {
try AWSRequest(
try AWSHTTPRequest(
operation: operationName,
path: path,
httpMethod: httpMethod,
method: httpMethod,
configuration: serviceConfig
)
},
processResponse: { response in
return try await self.validate(operation: operationName, response: response, serviceConfig: serviceConfig)
return try await self.processResponse(
operation: operationName,
response: response,
serviceConfig: serviceConfig,
logger: logger
)
},
config: serviceConfig,
logger: logger
Expand Down Expand Up @@ -355,17 +368,17 @@
return try await self.execute(
operation: operationName,
createRequest: {
try AWSRequest(
try AWSHTTPRequest(
operation: operationName,
path: path,
httpMethod: httpMethod,
method: httpMethod,
input: input,
hostPrefix: hostPrefix,
configuration: serviceConfig
)
},
processResponse: { response in
return try await self.validate(operation: operationName, response: response, serviceConfig: serviceConfig)
return try await self.processResponse(operation: operationName, response: response, serviceConfig: serviceConfig, logger: logger)
},
config: serviceConfig,
logger: logger
Expand All @@ -375,7 +388,7 @@
/// internal version of execute
internal func execute<Output>(
operation operationName: String,
createRequest: @escaping () throws -> AWSRequest,
createRequest: @escaping () throws -> AWSHTTPRequest,
processResponse: @escaping (AWSHTTPResponse) async throws -> Output,
config: AWSServiceConfig,
logger: Logger = AWSClient.loggingDisabled
Expand All @@ -396,18 +409,17 @@
// construct signer
let signer = AWSSigner(credentials: credential, name: config.signingName, region: config.region.rawValue)
// create request and sign with signer
let awsRequest = try createRequest()
.applyMiddlewares(config.middlewares + self.middlewares, config: config)
.createHTTPRequest(signer: signer, serviceConfig: config)
// send request to AWS and process result
let streaming = awsRequest.body.isStreaming
var request = try createRequest()
.applyMiddlewares(config.middlewares + self.middlewares, context: .init(operation: operationName, serviceConfig: config))
request.signHeaders(signer: signer, serviceConfig: config)
try Task.checkCancellation()
// apply middleware and sign
let response = try await self.invoke(
request: awsRequest,
request: request,
operation: operationName,
with: config,
logger: logger,
processResponse: processResponse,
streaming: streaming
processResponse: processResponse
)
logger.trace("AWS Response")
Metrics.Timer(
Expand All @@ -431,30 +443,25 @@

func invoke<Output>(
request: AWSHTTPRequest,
operation operationName: String,
with serviceConfig: AWSServiceConfig,
logger: Logger,
processResponse: @escaping (AWSHTTPResponse) async throws -> Output,
streaming: Bool
processResponse: @escaping (AWSHTTPResponse) async throws -> Output
) async throws -> Output {
var attempt = 0
while true {
do {
let response = try await self.httpClient.execute(request: request, timeout: serviceConfig.timeout, logger: logger)
// if it returns an HTTP status code outside 2xx then throw an error
.applyMiddlewares(serviceConfig.middlewares + self.middlewares, context: .init(operation: operationName, serviceConfig: serviceConfig))
// if response has an HTTP status code outside 2xx then throw an error
guard (200..<300).contains(response.status.code) else {
let error = try await self.createError(for: response, serviceConfig: serviceConfig, logger: logger)
let error = await self.createError(for: response, serviceConfig: serviceConfig, logger: logger)
throw error
}

let output = try await processResponse(response)
return output
} catch {
// if streaming and the error returned is an AWS error fail immediately. Do not attempt
// to retry as the streaming function will not know you are retrying
if streaming,
error is AWSErrorType || error is AWSRawError
{
throw error
}
// If I get a retry wait time for this error then attempt to retry request
if case .retry(let retryTime) = self.retryPolicy.getRetryWaitTime(error: error, attempt: attempt) {
logger.trace("Retrying request", metadata: [
Expand Down Expand Up @@ -502,7 +509,7 @@
)
let signer = try await self.createSigner(serviceConfig: serviceConfig, logger: logger)
guard let cleanURL = signer.processURL(url: url) else {
throw AWSClient.ClientError.invalidURL

Check warning on line 512 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L512

Added line #L512 was not covered by tests
}
return signer.signURL(url: cleanURL, method: httpMethod, headers: headers, expires: expires)
}
Expand Down Expand Up @@ -532,14 +539,14 @@
)
let signer = try await self.createSigner(serviceConfig: serviceConfig, logger: logger)
guard let cleanURL = signer.processURL(url: url) else {
throw AWSClient.ClientError.invalidURL

Check warning on line 542 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L542

Added line #L542 was not covered by tests
}
let bodyData: AWSSigner.BodyData?
switch body.storage {
case .byteBuffer(let buffer):
bodyData = .byteBuffer(buffer)
case .asyncSequence:
bodyData = nil

Check warning on line 549 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L549

Added line #L549 was not covered by tests
}
return signer.signHeaders(url: cleanURL, method: httpMethod, headers: headers, body: bodyData)
}
Expand All @@ -553,61 +560,67 @@
// response validator
extension AWSClient {
/// Generate an AWS Response from the operation HTTP response and return the output shape from it. This is only every called if the response includes a successful http status code
internal func validate<Output: AWSDecodableShape>(
internal func processResponse<Output: AWSDecodableShape>(
operation operationName: String,
response: AWSHTTPResponse,
serviceConfig: AWSServiceConfig
serviceConfig: AWSServiceConfig,
logger: Logger
) async throws -> Output {
assert((200..<300).contains(response.status.code), "Shouldn't get here if error was returned")

let raw = Output._options.contains(.rawPayload) == true
let awsResponse = try await AWSResponse(from: response, streaming: raw)
.applyMiddlewares(serviceConfig.middlewares + middlewares, config: serviceConfig)
var response = response
if !raw {
try await response.collateBody()
}
return try response.generateOutputShape(operation: operationName, serviceProtocol: serviceConfig.serviceProtocol)
}

return try awsResponse.generateOutputShape(operation: operationName, serviceProtocol: serviceConfig.serviceProtocol)
/// Generate an AWS Response from the operation HTTP response and return the output shape from it. This is only ever called if the response includes a successful http status code
internal func processEmptyResponse(
operation operationName: String,
response: AWSHTTPResponse,
serviceConfig: AWSServiceConfig,
logger: Logger
) async throws {
// flush response body contents to complete response read
for try await _ in response.body {}
}

/// Create error from HTTPResponse. This is only called if we received an unsuccessful http status code.
internal func createError(for response: AWSHTTPResponse, serviceConfig: AWSServiceConfig, logger: Logger) async throws -> Error {
internal func createError(for response: AWSHTTPResponse, serviceConfig: AWSServiceConfig, logger: Logger) async -> Error {
// if we can create an AWSResponse and create an error from it return that
let awsResponse: AWSResponse
var response = response
do {
awsResponse = try await AWSResponse(from: response, streaming: false)
try await response.collateBody()
} catch {
// else return "Unhandled error message" with rawBody attached
let context = AWSErrorContext(
message: "Unhandled Error",
responseCode: response.status,
headers: response.headers
)
return AWSRawError(rawBody: nil, context: context)

Check warning on line 601 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L595-L601

Added lines #L595 - L601 were not covered by tests
}
do {
let awsResponseWithMiddleware = try awsResponse.applyMiddlewares(serviceConfig.middlewares + middlewares, config: serviceConfig)
if let error = awsResponseWithMiddleware.generateError(
serviceConfig: serviceConfig,
logLevel: options.errorLogLevel,
logger: logger
) {
return error
} else {
// else return "Unhandled error message" with rawBody attached
let context = AWSErrorContext(
message: "Unhandled Error",
responseCode: response.status,
headers: response.headers
)
let responseBody: String?
switch awsResponseWithMiddleware.body.storage {
case .byteBuffer(let buffer):
responseBody = String(buffer: buffer)
default:
responseBody = nil
}
return AWSRawError(rawBody: responseBody, context: context)
}
} catch {
if let error = response.generateError(
serviceConfig: serviceConfig,
logLevel: options.errorLogLevel,
logger: logger
) {
return error
} else {
// else return "Unhandled error message" with rawBody attached
let context = AWSErrorContext(
message: "Unhandled Error",
responseCode: response.status,
headers: response.headers
)
let responseBody: String?
switch response.body.storage {
case .byteBuffer(let buffer):
responseBody = String(buffer: buffer)
default:
responseBody = nil

Check warning on line 621 in Sources/SotoCore/AWSClient.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/AWSClient.swift#L621

Added line #L621 was not covered by tests
}
return AWSRawError(rawBody: responseBody, context: context)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SotoCore/Encoder/QueryEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public struct QueryEncoder {
static let queryAllowedCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")

private static func urlEncodeQueryParam(_ value: String) -> String {
return value.addingPercentEncoding(withAllowedCharacters: AWSRequest.queryAllowedCharacters) ?? value
return value.addingPercentEncoding(withAllowedCharacters: AWSHTTPRequest.queryAllowedCharacters) ?? value
}

// generate string from
Expand Down
2 changes: 1 addition & 1 deletion Sources/SotoCore/Encoder/ResponseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,34 @@
let header: String
let message: String

static func headerNotFound(_ header: String) -> Self { .init(header: header, message: "Header not found") }
static func typeMismatch(_ header: String, expectedType: String) -> Self { .init(header: header, message: "Cannot convert header to \(expectedType)") }

Check warning on line 25 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L24-L25

Added lines #L24 - L25 were not covered by tests
}

public struct ResponseDecodingContainer {
let response: AWSResponse
let response: AWSHTTPResponse

public func decode<Value: RawRepresentable>(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value where Value.RawValue == String {
guard let headerValue = response.headers[header].first else {
throw HeaderDecodingError.headerNotFound(header)
}
if let result = Value(rawValue: headerValue) {
return result
} else {
throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)")
}
}

Check warning on line 40 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L31-L40

Added lines #L31 - L40 were not covered by tests

public func decode<Value: LosslessStringConvertible>(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value {
guard let headerValue = response.headers[header].first else {
throw HeaderDecodingError.headerNotFound(header)

Check warning on line 44 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L44

Added line #L44 was not covered by tests
}
if let result = Value(headerValue) {
return result
} else {
throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)")
}
}

Check warning on line 51 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L49-L51

Added lines #L49 - L51 were not covered by tests

public func decodeStatus<Value: FixedWidthInteger>(_: Value.Type = Value.self) -> Value {
return Value(self.response.status.code)
Expand All @@ -58,13 +58,13 @@
return self.response.body
}

public func decodeEventStream<Event>() -> AWSEventStream<Event> {
return .init(self.response.body)
}

Check warning on line 63 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L61-L63

Added lines #L61 - L63 were not covered by tests

public func decode(_ type: Date.Type = Date.self, forHeader header: String) throws -> Date {
guard let headerValue = response.headers[header].first else {
throw HeaderDecodingError.headerNotFound(header)

Check warning on line 67 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L67

Added line #L67 was not covered by tests
}
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
Expand All @@ -73,45 +73,45 @@
if let result = dateFormatter.date(from: headerValue) {
return result
} else {
throw HeaderDecodingError.typeMismatch(header, expectedType: "Date")
}
}

Check warning on line 78 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L76-L78

Added lines #L76 - L78 were not covered by tests

public func decodeIfPresent<Value: RawRepresentable>(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value? where Value.RawValue == String {
guard let headerValue = response.headers[header].first else { return nil }
if let result = Value(rawValue: headerValue) {
return result
} else {
throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)")
}
}

Check warning on line 87 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L80-L87

Added lines #L80 - L87 were not covered by tests

public func decodeIfPresent<Value: LosslessStringConvertible>(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value? {
guard let headerValue = response.headers[header].first else { return nil }
if let result = Value(headerValue) {
return result
} else {
throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)")
}
}

Check warning on line 96 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L89-L96

Added lines #L89 - L96 were not covered by tests

public func decodeIfPresent(_ type: Date.Type = Date.self, forHeader header: String) throws -> Date? {
guard let headerValue = response.headers[header].first else { return nil }
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
if let result = dateFormatter.date(from: headerValue) {
return result
} else {
throw HeaderDecodingError.typeMismatch(header, expectedType: "Date")
}
}

Check warning on line 109 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L98-L109

Added lines #L98 - L109 were not covered by tests

public func decodeIfPresent(_ type: [String: String].Type = [String: String].self, forHeader header: String) throws -> [String: String]? {
let headers = self.response.headers.compactMap { $0.name.hasPrefix(header) ? $0 : nil }
if headers.count == 0 {
return nil

Check warning on line 114 in Sources/SotoCore/Encoder/ResponseContainer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/Encoder/ResponseContainer.swift#L114

Added line #L114 was not covered by tests
}
return [String: String](headers.map { (key: String($0.name.dropFirst(header.count)), value: $0.value) }) { first, _ in first }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,29 +54,29 @@
public func collect(upTo length: Int) async throws -> ByteBuffer {
switch self.storage {
case .byteBuffer(let buffer):
return buffer

Check warning on line 57 in Sources/SotoCore/HTTP/AWSHTTPBody.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/HTTP/AWSHTTPBody.swift#L57

Added line #L57 was not covered by tests
case .asyncSequence(let sequence, _):
return try await sequence.collect(upTo: length)
}
}

public var length: Int? {
switch self.storage {
case .byteBuffer(let buffer):
return buffer.readableBytes
case .asyncSequence(_, let length):
return length
}
}

Check warning on line 70 in Sources/SotoCore/HTTP/AWSHTTPBody.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/HTTP/AWSHTTPBody.swift#L63-L70

Added lines #L63 - L70 were not covered by tests

public var isStreaming: Bool {
switch self.storage {
case .byteBuffer:
return false
case .asyncSequence:
return true
}
}

Check warning on line 79 in Sources/SotoCore/HTTP/AWSHTTPBody.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/HTTP/AWSHTTPBody.swift#L72-L79

Added lines #L72 - L79 were not covered by tests
}

extension AWSHTTPBody: AsyncSequence {
Expand All @@ -86,7 +86,7 @@
public func makeAsyncIterator() -> AsyncIterator {
switch self.storage {
case .byteBuffer(let buffer):
return AnyAsyncSequence(buffer.asyncSequence(chunkSize: buffer.readableBytes)).makeAsyncIterator()

Check warning on line 89 in Sources/SotoCore/HTTP/AWSHTTPBody.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/HTTP/AWSHTTPBody.swift#L89

Added line #L89 was not covered by tests
case .asyncSequence(let sequence, _):
return sequence.makeAsyncIterator()
}
Expand All @@ -96,41 +96,7 @@
extension AWSHTTPBody: Decodable {
// AWSHTTPBody has to conform to Decodable so I can add it to AWSShape objects (which conform to Decodable). But we don't want the
// Encoder/Decoder ever to process a AWSPayload
public init(from decoder: Decoder) throws {
preconditionFailure("Cannot decode an AWSHTTPBody")
}

Check warning on line 101 in Sources/SotoCore/HTTP/AWSHTTPBody.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SotoCore/HTTP/AWSHTTPBody.swift#L99-L101

Added lines #L99 - L101 were not covered by tests
}

/// HTTP Request
struct AWSHTTPRequest {
let url: URL
let method: HTTPMethod
let headers: HTTPHeaders
let body: AWSHTTPBody

init(url: URL, method: HTTPMethod, headers: HTTPHeaders = [:], body: AWSHTTPBody = .init()) {
self.url = url
self.method = method
self.headers = headers
self.body = body
}
}

/// Generic HTTP Response returned from HTTP Client
struct AWSHTTPResponse: Sendable {
/// Initialize AWSHTTPResponse
init(status: HTTPResponseStatus, headers: HTTPHeaders, body: AWSHTTPBody = .init()) {
self.status = status
self.headers = headers
self.body = body
}

/// The HTTP status for this response.
var status: HTTPResponseStatus

/// The HTTP headers of this response.
var headers: HTTPHeaders

/// The body of this HTTP response.
var body: AWSHTTPBody
}
Loading
Loading