Skip to content

Commit ed3ee0e

Browse files
authored
Merge pull request #54 from GoodRequest/async-semaphore
fix: Fix actor isolation crashes and data races
2 parents d01d94d + f34b6fd commit ed3ee0e

File tree

7 files changed

+294
-92
lines changed

7 files changed

+294
-92
lines changed

Sources/GoodNetworking/Interception/AuthenticationInterceptor.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ import Foundation
1414
public final class AuthenticationInterceptor<AuthenticatorType: Authenticator>: Interceptor, @unchecked Sendable {
1515

1616
private let authenticator: AuthenticatorType
17-
private let lock: AsyncLock
17+
private let lock: AsyncSemaphore
1818

1919
public init(authenticator: AuthenticatorType) {
2020
self.authenticator = authenticator
21-
self.lock = AsyncLock()
21+
self.lock = AsyncSemaphore(value: 1)
2222
}
2323

2424
public func adapt(urlRequest: inout URLRequest) async throws(NetworkError) {
25-
await lock.lock()
26-
defer { lock.unlock() }
27-
25+
await lock.wait()
26+
defer { lock.signal() }
27+
2828
if let credential = await authenticator.getCredential() {
2929
if let refreshableCredential = credential as? RefreshableCredential, refreshableCredential.requiresRefresh {
3030
try await refresh(credential: credential)
@@ -47,8 +47,8 @@ public final class AuthenticationInterceptor<AuthenticatorType: Authenticator>:
4747
// Stop further authentication with possibly invalid credential.
4848
// If a refresh is already in progress, stopping other requests
4949
// here will ensure further retries will contain the latest credentials.
50-
await lock.lock()
51-
defer { lock.unlock() }
50+
await lock.wait()
51+
defer { lock.signal() }
5252

5353
// A credential is available
5454
guard let credential = await authenticator.getCredential() else {

Sources/GoodNetworking/Logging/DataTaskLogging.swift

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal extension DataTaskProxy {
2626
\(prettyPrintMessage(data: task.originalRequest?.httpBody))
2727
2828
📦 Received data:
29-
\(prettyPrintMessage(data: receivedData))
29+
\(prettyPrintMessage(data: receivedData, mimeType: task.response?.mimeType))
3030
"""
3131
}
3232

@@ -53,9 +53,9 @@ internal extension DataTaskProxy {
5353
.joined(separator: "\n")
5454
}
5555

56-
@NetworkActor private func prettyPrintMessage(data: Data?) -> String {
56+
@NetworkActor private func prettyPrintMessage(data: Data?, mimeType: String? = "text/plain") -> String {
5757
guard let data else { return "" }
58-
58+
guard plainTextMimeTypeHeuristic(mimeType) else { return "🏞️ Detected MIME type is not plain text" }
5959
guard data.count < Self.maxLogSizeBytes else {
6060
return "💡 Data size is too big (\(data.count) bytes), console limit is \(Self.maxLogSizeBytes) bytes"
6161
}
@@ -67,7 +67,9 @@ internal extension DataTaskProxy {
6767
}
6868

6969
if let string = String(data: data, encoding: .utf8) {
70-
if let jsonData = try? JSONSerialization.jsonObject(with: data, options: []),
70+
let mimeContainsJson = mimeType?.contains("json")
71+
if mimeContainsJson ?? true,
72+
let jsonData = try? JSONSerialization.jsonObject(with: data, options: []),
7173
let prettyPrintedData = try? JSONSerialization.data(withJSONObject: jsonData, options: serializationOptions),
7274
let prettyPrintedString = String(data: prettyPrintedData, encoding: .utf8) {
7375
return prettyPrintedString
@@ -79,4 +81,25 @@ internal extension DataTaskProxy {
7981
return "🔍 Couldn't decode data as UTF-8"
8082
}
8183

84+
@NetworkActor private func plainTextMimeTypeHeuristic(_ mimeType: String?) -> Bool {
85+
guard let mimeType else { return false }
86+
87+
let knownPlainTextMimeTypes = ["javascript", "yaml", "toml", "sql", "graphql", "markdown", "urlencoded"]
88+
89+
let isTextMimeType = mimeType.hasPrefix("text/")
90+
let isXml = mimeType.hasSuffix("+xml") || mimeType.contains("xml")
91+
let isJson = mimeType.hasSuffix("+json") || mimeType.contains("json")
92+
let isTextBased = mimeType.containsOneOf(knownPlainTextMimeTypes)
93+
94+
return isTextMimeType || isXml || isJson || isTextBased
95+
}
96+
97+
}
98+
99+
extension String {
100+
101+
func containsOneOf(_ strings: [String]) -> Bool {
102+
strings.contains { self.contains($0) }
103+
}
104+
82105
}

Sources/GoodNetworking/Session/DataTaskProxy.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ import Foundation
5454
self.receivedError = URLError(.unknown)
5555
}
5656

57-
logger.logNetworkEvent(
58-
message: prepareRequestInfo(),
59-
level: receivedError == nil ? .debug : .warning,
60-
file: #file,
61-
line: #line
62-
)
57+
Task { @NetworkActor in
58+
logger.logNetworkEvent(
59+
message: prepareRequestInfo(),
60+
level: receivedError == nil ? .debug : .warning,
61+
file: #file,
62+
line: #line
63+
)
64+
}
6365

6466
continuation?.resume()
6567
continuation = nil

Sources/GoodNetworking/Session/NetworkSession.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import Foundation
6767
self.configuration = configuration
6868
self.delegateQueue = operationQueue
6969

70-
// create URLSession lazily, isolated on @NetworkActor, when requested first time
70+
// create URLSession lazily, isolated to @NetworkActor, when requested first time
7171

7272
super.init()
7373
}

Sources/GoodNetworking/Utilities/AsyncLock.swift

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)