Search code examples
iosswifthttpaws-api-gatewaysoto

AsyncHTTPClient Requests not working (Swift iOS)


Brand new to Swift and iOS development (started learning this weekend).

I am trying to make a generic HTTP Client wrapper for signing and sending requests to API Gateway (AWS) using SotoSignerV4 and AsyncHTTPClient. I have followed an excellent guide to get some boiler plate ideas on how to achieve this, but when I go to test this approach in a unit test (XCTestCase), it does not work at all as expected. It seems as if the request isn't even executed (in fact I know it isn't, because I can check the logs on my Lambda application - the request isn't reaching lambda).

Here's the bulk of my code:

private func request<TEntity: Codable>(
        type: TEntity.Type,
        body: Codable? = nil,
        resource: String,
        method: HTTPMethod) throws -> TEntity {
    
    // A bunch of AWS Sig4 signing stuff happens here, already tested and working...    
    
    let timeout = HTTPClient.Configuration.Timeout(
        connect: .seconds(30), read: .seconds(30))
    let httpClient: HTTPClient = HTTPClient(
        eventLoopGroupProvider: .createNew,
        configuration: HTTPClient.Configuration(timeout: timeout))
        
    var entity: TEntity? = nil;
    var responseString: String? = nil;
        
    let request = try! HTTPClient.Request(
        url: processedUrl,
        method: method,
        headers: signedHeaders,
        body: requestPayload)
        
    let future = httpClient.execute(request: request)
           
    future.whenComplete {
          
        result in
        switch result {
        case .success(let response):
            guard let responseBuffer = response.body, response.status == .ok
            else { return }
            let responseBody = Data(buffer: responseBuffer)
            responseString = String(decoding: responseBody, as: UTF8.self)
        case .failure(_):
            return
        }
            
    }
        
    try httpClient.syncShutdown()
        
    entity = try! JSONDecoder().decode(TEntity.self, from: Data(responseString!.utf8));
        
    return entity!;
}

The failure is on the entity assignment line, stating,

Fatal error: Unexpectedly found nil while unwrapping an Optional value: file MyFramework/AWSSignedClient.swift, line 158

The optional it can't unwrap is the response string, meaning that the code in the .success case was never hit. I have tried testing the error case as well by capturing a string representing the error, and not even that works. It's as if the callback associated with whenComplete is completely skipped, the request never being made. I have also tried using URLSession.shared.dataTask, with the same results - no request is ever made.

Please help! What am I doing wrong with the HTTPClient?


Solution

  • The problem you have here is you are expecting your code to run synchronously when you are calling asynchronous code. You call future.whenComplete but shutdown the client immediately after. The request never gets a chance to complete.

    You can either setup your function so that it calls a callback when the request is finished (called from your whenComplete closure) or you could add a future.wait() which will wait for the request to complete before continuing. The first option is probably preferable as the second option will stall the thread you are running on while it waits for a response.

    Or a third option is have the function return a EventLoopFuture<TEntity>.

    return httpClient.execute(request: request)
        .flatMapThrowing { response in
            guard let responseBuffer = response.body, response.status == .ok
                else { throw Error.requestFailed }
            let responseBody = Data(buffer: responseBuffer)
            return try JSONDecoder().decode(TEntity.self, responseBody)
        }
    

    The reference docs for EventLoopFuture https://apple.github.io/swift-nio/docs/current/NIO/Classes/EventLoopFuture.html are pretty comprehensive and are worthwhile reading if you are going to be interacting with them.

    If you choose the first or third options you cannot shutdown the client inside your function. You will need to do it at a time when you are sure the request has finished. It is probably easier to keep an instance of HTTPClient around all the time