contentful.swift icon indicating copy to clipboard operation
contentful.swift copied to clipboard

Contentful API breaks Swift Concurrency by calling completion handlers more than once

Open tristan-warner-smith opened this issue 3 years ago • 0 comments

  • contentful.swift version number: 5.5.2
  • Xcode version number: 13.3
  • Target operating system(s) and version number(s)
    • [X] iOS: 14.5, 15.4
    • [ ] tvOS:
    • [ ] watchOS:
    • [ ] macOS:
  • Package manager:
    • [ ] Carthage
    • [ ] Cocoapods

Using the Contentful client SDK results in app crashes when completion-based calls fail to decode.

I just encountered this because I've wrapped fetchArray(of in a Task and this triggers Fatal error: SWIFT TASK CONTINUATION MISUSE: fetchArray(of:) tried to resume its continuation more than once, throwing Unknown error occured during decoding.!

The issue I saw is in Client.swift handleJSON<DecodableType: Decodable> where if there's a decode failure the completion is called twice with potentially different errors. This seems to be related to a desire to churnLinks regardless of the failure - but if it failed to decode anyway, we're done there's no value in resolving other failures, we can just go fix our issue and move on, right?

If a failure happens on the original decode the failure completion is called but not returned so it continues to a second decoded check that fails and again calls completion(.failure(error).

The two completion calls are in Client.swift:476 and 500 in Contentful 5.5.2.

This is just how I came across it but any decoding error should reproduce:

  • Create a parent child model - 1 Article has many Pages for example
  • Create an article with no pages
  • Have your decode be something like: self.pages = try fields.decode([Link].self, forKey: .pages) where it assumes non-optionals
  • Wrap Contentful API to be async-awaitable
extension Contentful.Client {
    // MARK: - Making Contentful fetches async
    
    func fetchArray<ResourceType: EntryDecodable & FieldKeysQueryable>(of resourceType: ResourceType.Type) async throws -> [ResourceType] {
        
        try await withCheckedThrowingContinuation { continuation in
            fetchArray(of: resourceType) { result in
                switch result {
                case .success(let results):
                    continuation.resume(returning: results.items.map { $0 as ResourceType })
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
    
    func fetchArray<EntryType>(
        of entryType: EntryType.Type,
        matching query: QueryOn<EntryType>
    ) async throws -> [EntryType] {
        try await withCheckedThrowingContinuation { continuation in
            fetchArray(of: entryType, matching: query) { result in
                switch result {
                case .success(let results):
                    continuation.resume(returning: results.items.map { $0 as EntryType })
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
}
  • Make a call that will return the parent-child types: let articles = try await client.fetchArray(of: Article.self)
  • See fatal as above SWIFT TASK CONTINUATION MISUSE: fetchArray(of:) tried to resume its continuation more than once, throwing Unknown error occured during decoding.!

tristan-warner-smith avatar May 16 '22 10:05 tristan-warner-smith