diff --git a/Sources/Conduit/Auth/OAuth2RequestPipelineMiddleware.swift b/Sources/Conduit/Auth/OAuth2RequestPipelineMiddleware.swift index 1fe6445..0ad38f6 100644 --- a/Sources/Conduit/Auth/OAuth2RequestPipelineMiddleware.swift +++ b/Sources/Conduit/Auth/OAuth2RequestPipelineMiddleware.swift @@ -23,8 +23,6 @@ public struct OAuth2RequestPipelineMiddleware: RequestPipelineMiddleware { /// against lengthy session locks when terminating mid-flight. This will allow the host process to quickly pick /// up where the other process left off, if it needs to. Defaults to 30 seconds. public var tokenRefreshLockRelinquishInterval: TimeInterval = 30 - /// Kill switch for atomic in-process refresh serialization; static because instances are per-client value copies. Defaults to true. - public static var refreshClaimCoordinationEnabled = true let clientConfiguration: OAuth2ClientConfiguration let authorization: OAuth2Authorization let tokenStorage: OAuth2TokenStore @@ -315,3 +313,23 @@ public struct OAuth2RequestPipelineMiddleware: RequestPipelineMiddleware { } } + +private let refreshClaimCoordinationFlagLock = NSLock() +private var storedRefreshClaimCoordinationEnabled = true + +extension OAuth2RequestPipelineMiddleware { + /// Kill switch for atomic in-process refresh serialization; static because instances are per-client value copies. Defaults to true. + /// Lock-backed so it can be toggled at runtime while request pipelines read it. + public static var refreshClaimCoordinationEnabled: Bool { + get { + refreshClaimCoordinationFlagLock.lock() + defer { refreshClaimCoordinationFlagLock.unlock() } + return storedRefreshClaimCoordinationEnabled + } + set { + refreshClaimCoordinationFlagLock.lock() + defer { refreshClaimCoordinationFlagLock.unlock() } + storedRefreshClaimCoordinationEnabled = newValue + } + } +} diff --git a/Sources/Conduit/Networking/URLSessionClient.swift b/Sources/Conduit/Networking/URLSessionClient.swift index ee0736a..3d87be4 100644 --- a/Sources/Conduit/Networking/URLSessionClient.swift +++ b/Sources/Conduit/Networking/URLSessionClient.swift @@ -87,6 +87,10 @@ public struct URLSessionClient: URLSessionClientType { throw URLSessionClientError.requestTimeout } if let error = result.error { + // The session's own request timeout races the semaphore above; report both as requestTimeout. + if (error as? URLError)?.code == .timedOut { + throw URLSessionClientError.requestTimeout + } throw error } guard let response = result.response else { diff --git a/Tests/ConduitTests/Networking/URLSessionClientTests.swift b/Tests/ConduitTests/Networking/URLSessionClientTests.swift index 4699657..0ab1491 100644 --- a/Tests/ConduitTests/Networking/URLSessionClientTests.swift +++ b/Tests/ConduitTests/Networking/URLSessionClientTests.swift @@ -31,10 +31,11 @@ class URLSessionClientTests: XCTestCase { /// Verify sesson client throws error for timeout func testBlockingTimeout() throws { - let configuration = URLSessionConfiguration.default + let configuration = URLSessionConfiguration.ephemeral configuration.timeoutIntervalForRequest = 0.5 + configuration.protocolClasses = [StalledURLProtocol.self] let client = URLSessionClient(sessionConfiguration: configuration, delegateQueue: OperationQueue()) - let request = try URLRequest(url: URL(absoluteString: "https://httpbingo.org/delay/1")) + let request = try URLRequest(url: URL(absoluteString: "https://stalled.invalid/delay")) XCTAssertThrowsError(try client.begin(request: request), "Request did not timeout") { error in XCTAssertEqual(error as? URLSessionClientError, .requestTimeout) } @@ -358,3 +359,11 @@ private class TransformingResponseMiddleware: ResponsePipelineMiddleware { } } + +/// Never completes a request, forcing the timeout path deterministically. +private class StalledURLProtocol: URLProtocol { + override class func canInit(with request: URLRequest) -> Bool { true } + override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } + override func startLoading() {} + override func stopLoading() {} +}