contentful.swift
contentful.swift copied to clipboard
Contentful API breaks Swift Concurrency by calling completion handlers more than once
- 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.!