Skip to content

Commit

Permalink
Merge request and response types (#569)
Browse files Browse the repository at this point in the history
* reorder execute

* process response in execute

* Collapse AWSHTTPResponse and AWSResponse into one

* Collpase AWSRequest and AWSHTTPRequest into one

* Add HTTP to request and response type names

* Remove new middleware until we need it

No need to include in this PR

* Remove re-org to make PR review easier

* Changes from PR comments

* Another fix
  • Loading branch information
adam-fowler committed Apr 5, 2024
1 parent 224a9e3 commit d8dac50
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 451 deletions.
151 changes: 82 additions & 69 deletions Sources/SotoCore/AWSClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,18 +247,22 @@ extension AWSClient {
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 @@ extension AWSClient {
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 @@ extension AWSClient {
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 @@ extension AWSClient {
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 @@ extension AWSClient {
/// 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 @@ extension AWSClient {
// 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 @@ extension AWSClient {

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 @@ -553,26 +560,37 @@ extension AWSClient {
// 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(
Expand All @@ -582,32 +600,27 @@ extension AWSClient {
)
return AWSRawError(rawBody: nil, context: context)
}
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
}
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 @@ -26,7 +26,7 @@ public struct HeaderDecodingError: Error {
}

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,37 +100,3 @@ extension AWSHTTPBody: Decodable {
preconditionFailure("Cannot decode an AWSHTTPBody")
}
}

/// 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

0 comments on commit d8dac50

Please sign in to comment.