import Foundation import Testing @testable import CodexBarCore private final class OpenCodeGoRequestRecorder: @unchecked Sendable { private let lock = NSLock() private var storage: [Value] = [] func append(_ value: Value) { self.lock.lock() defer { self.lock.unlock() } self.storage.append(value) } var values: [Value] { self.lock.lock() defer { self.lock.unlock() } return self.storage } } private final class OpenCodeGoContinuationBox: @unchecked Sendable { private let lock = NSLock() private var continuation: CheckedContinuation? func wait(onReady: @Sendable () -> Void) async -> Value { await withCheckedContinuation { continuation in self.lock.lock() self.continuation = continuation self.lock.unlock() onReady() } } func resume(returning value: Value) { self.lock.lock() let continuation = self.continuation self.continuation = nil self.lock.unlock() continuation?.resume(returning: value) } } @Suite(.serialized) struct OpenCodeGoUsageFetcherErrorTests { @Test func `dashboard URL uses normalized workspace ID`() { #expect( OpenCodeGoUsageFetcher.dashboardURL(workspaceID: "https://opencode.ai/workspace/wrk_abc123/go") .absoluteString == "https://opencode.ai/workspace/wrk_abc123/go") #expect( OpenCodeGoUsageFetcher.dashboardURL(workspaceID: "workspace=wrk_def456") .absoluteString == "https://opencode.ai/workspace/wrk_def456/go") #expect( OpenCodeGoUsageFetcher.dashboardURL(workspaceID: nil) .absoluteString == "https://opencode.ai") } private struct UsageWindow { let percent: Double let resetInSec: Int } private func makeSession() -> URLSession { let config = URLSessionConfiguration.ephemeral config.protocolClasses = [OpenCodeGoStubURLProtocol.self] return URLSession(configuration: config) } @Test func `redirect guard allows only same-host https redirects`() { #expect(OpenCodeGoUsageFetcher.allowsRedirect( from: URL(string: "https://opencode.ai/_server"), to: URL(string: "https://opencode.ai/workspace/wrk_TEST123/go"))) #expect(!OpenCodeGoUsageFetcher.allowsRedirect( from: URL(string: "https://opencode.ai/_server"), to: URL(string: "https://evil.example/steal"))) #expect(!OpenCodeGoUsageFetcher.allowsRedirect( from: URL(string: "https://opencode.ai/_server"), to: URL(string: "http://opencode.ai/insecure"))) } @Test func `extracts api error from detail field`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } let body = #"{"detail":"Workspace missing"}"# return Self.makeResponse(url: url, body: body, statusCode: 500, contentType: "application/json") } do { _ = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) Issue.record("Expected OpenCodeGoUsageError.apiError") } catch let error as OpenCodeGoUsageError { switch error { case let .apiError(message): #expect(message.contains("HTTP 500")) #expect(message.contains("Workspace missing")) default: Issue.record("Expected apiError, got: \(error)") } } } @Test func `workspace get missing ids falls back to post before loading go page`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let requests = OpenCodeGoRequestRecorder() OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } requests.append("\(request.httpMethod ?? "GET") \(url.path)") let workspaceServerID = "def39973159c7f0483d8793a822b8dbb10d067e12c65455fcb4608459ba0234f" if url.query?.contains(workspaceServerID) == true, request.httpMethod?.uppercased() == "GET" { return Self.makeResponse( url: url, body: #"{"ok":true}"#, statusCode: 200, contentType: "application/json") } if url.path == "/_server", request.httpMethod?.uppercased() == "POST", request.value(forHTTPHeaderField: "X-Server-Id") == workspaceServerID { return Self.makeResponse( url: url, body: #"{"data":[{"id":"wrk_TEST123"}]}"#, statusCode: 200, contentType: "application/json") } return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 22, resetInSec: 300), weekly: UsageWindow(percent: 44, resetInSec: 3600), monthly: UsageWindow(percent: 55, resetInSec: 7200)), statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, includeZenBalance: false, session: self.makeSession()) #expect(snapshot.rollingUsagePercent == 22) #expect(snapshot.weeklyUsagePercent == 44) #expect(snapshot.monthlyUsagePercent == 55) #expect(requests.values == [ "GET /_server", "POST /_server", "GET /workspace/wrk_TEST123/go", ]) } @Test func `workspace get public actor error is treated as invalid credentials without post retry`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let methods = OpenCodeGoRequestRecorder() OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } methods.append(request.httpMethod ?? "GET") let body = [ #";0x00000263;((self.$R=self.$R||{})["server-fn:test"]=[],"#, #"($R=>$R[0]=Object.assign(new Error("actor of type \"public\" is not associated with an account"),"#, #"{stack:"Error: actor of type \"public\" is not associated with an account"}))"#, #"($R["server-fn:test"]))"#, ].joined() return Self.makeResponse( url: url, body: body, statusCode: 200, contentType: "text/javascript") } do { _ = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, session: self.makeSession()) Issue.record("Expected OpenCodeGoUsageError.invalidCredentials") } catch let error as OpenCodeGoUsageError { switch error { case .invalidCredentials: break default: Issue.record("Expected invalidCredentials, got: \(error)") } } #expect(methods.values == ["GET"]) } @Test func `go page missing usage fields returns parse failed without post retry`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let methods = OpenCodeGoRequestRecorder() OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } methods.append(request.httpMethod ?? "GET") return Self.makeResponse( url: url, body: "opencodeNo usage yet", statusCode: 200, contentType: "text/html") } do { _ = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) Issue.record("Expected OpenCodeGoUsageError.parseFailed") } catch let error as OpenCodeGoUsageError { switch error { case let .parseFailed(message): #expect(message.contains("Missing usage fields")) default: Issue.record("Expected parseFailed, got: \(error)") } } #expect(methods.values == ["GET", "GET", "GET"]) } @Test func `zen only account waits for balance beyond optional grace`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let observedPaths = OpenCodeGoRequestRecorder() OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } observedPaths.append(url.path) if url.path == "/workspace/wrk_TEST123" { Thread.sleep(forTimeInterval: 0.4) return Self.makeResponse( url: url, body: #"

現在の残高 $42.50

"#, statusCode: 200, contentType: "text/html") } return Self.makeResponse( url: url, body: "opencodeNo Go subscription usage", statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) let usage = snapshot.toUsageSnapshot() #expect(snapshot.isBalanceOnly) #expect(snapshot.zenBalanceUSD == 42.5) #expect(usage.primary == nil) #expect(usage.secondary == nil) #expect(usage.providerCost?.used == 42.5) #expect(usage.providerCost?.period == "Zen balance") #expect(observedPaths.values.count == 2) #expect(Set(observedPaths.values) == ["/workspace/wrk_TEST123/go", "/workspace/wrk_TEST123"]) } @Test func `zen only account fetches required balance when optional usage is disabled`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let observedPaths = OpenCodeGoRequestRecorder() OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } observedPaths.append(url.path) if url.path == "/workspace/wrk_TEST123" { return Self.makeResponse( url: url, body: #"

Current balance $23.75

"#, statusCode: 200, contentType: "text/html") } return Self.makeResponse( url: url, body: "opencodeNo Go subscription usage", statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, workspaceIDOverride: "wrk_TEST123", includeZenBalance: false, session: self.makeSession()) #expect(snapshot.isBalanceOnly) #expect(snapshot.zenBalanceUSD == 23.75) #expect(observedPaths.values == ["/workspace/wrk_TEST123/go", "/workspace/wrk_TEST123"]) } @Test func `zen only account propagates invalid credentials from required balance fetch`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } if url.path == "/workspace/wrk_TEST123" { return Self.makeResponse( url: url, body: "Unauthorized", statusCode: 401, contentType: "text/plain") } return Self.makeResponse( url: url, body: "opencodeNo Go subscription usage", statusCode: 200, contentType: "text/html") } do { _ = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=stale", timeout: 2, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) Issue.record("Expected invalid credentials to propagate.") } catch OpenCodeGoUsageError.invalidCredentials { // Expected. } catch { Issue.record("Expected invalidCredentials, got: \(error)") } } @Test func `zen only account falls back after final subscription parse failure`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } var rootTimeout: TimeInterval? OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } if url.path == "/workspace/wrk_TEST123" { rootTimeout = request.timeoutInterval return Self.makeResponse( url: url, body: #"

Current balance $17.25

"#, statusCode: 200, contentType: "text/html") } return Self.makeResponse( url: url, body: #""#, statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 12, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) let usage = snapshot.toUsageSnapshot() #expect(snapshot.isBalanceOnly) #expect(snapshot.zenBalanceUSD == 17.25) #expect(usage.primary == nil) #expect(usage.secondary == nil) #expect(usage.providerCost?.used == 17.25) #expect(rootTimeout == 12) } @Test func `zen only fallback promptly cancels when balance task ignores cancellation`() async throws { let balanceStarted = AsyncStream.makeStream(of: Void.self) let balanceContinuation = OpenCodeGoContinuationBox() let balanceTask = Task { await balanceContinuation.wait { balanceStarted.continuation.yield(()) } } let fallbackTask = Task { try await OpenCodeGoUsageFetcher.requiredZenBalanceFallback( from: balanceTask, for: .parseFailed("Missing usage fields."), request: OpenCodeGoUsageFetcher.ZenBalanceRequest( workspaceID: "wrk_TEST123", cookieHeader: "auth=test", timeout: 2, session: self.makeSession()), now: Date()) } var iterator = balanceStarted.stream.makeAsyncIterator() _ = await iterator.next() let start = ContinuousClock.now fallbackTask.cancel() do { _ = try await fallbackTask.value Issue.record("Expected cancellation to propagate.") } catch is CancellationError { // Expected. } catch { Issue.record("Expected CancellationError, got: \(error)") } #expect(start.duration(to: .now) < .milliseconds(500)) #expect(balanceTask.isCancelled) balanceContinuation.resume(returning: 42.5) #expect(try await balanceTask.value == 42.5) } @Test func `normalizes workspace override from URL into go page path`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let observedPaths = OpenCodeGoRequestRecorder() OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } observedPaths.append(url.path) return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_URL123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } _ = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, workspaceIDOverride: "https://opencode.ai/workspace/wrk_URL123/billing", session: self.makeSession()) #expect(observedPaths.values.count == 3) #expect(Set(observedPaths.values) == [ "/workspace/wrk_URL123/go", "/workspace/wrk_URL123", "/_server", ]) } @Test func `fetcher attaches optional zen balance from workspace root`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } if url.path == "/workspace/wrk_TEST123" { return Self.makeResponse( url: url, body: #"

現在の残高 $98.76

"#, statusCode: 200, contentType: "text/html") } return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) #expect(snapshot.zenBalanceUSD == 98.76) #expect(snapshot.toUsageSnapshot().providerCost?.period == "Zen balance") } @Test func `fetcher falls back to billing server when workspace page omits balance`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let observedRequests = OpenCodeGoRequestRecorder() OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } observedRequests.append(request) if url.path == "/workspace/wrk_TEST123" { return Self.makeResponse( url: url, body: "Workspace dashboard without hydrated billing data", statusCode: 200, contentType: "text/html") } if url.path == "/_server" { return Self.makeResponse( url: url, body: #"$R[0]={customerID:"cus_test",balance:$R[1]=9876000000,reload:!1}"#, statusCode: 200, contentType: "text/javascript") } return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 2, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) #expect(snapshot.zenBalanceUSD == 98.76) let billingRequest = try #require(observedRequests.values.first { $0.url?.path == "/_server" }) let billingURL = try #require(billingRequest.url) let components = try #require(URLComponents(url: billingURL, resolvingAgainstBaseURL: false)) let query = Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value ?? "") }) #expect(query["id"] == "c83b78a614689c38ebee981f9b39a8b377716db85c1fd7dbab604adc02d3313d") #expect(query["args"] == #"["wrk_TEST123"]"#) #expect(billingRequest.value(forHTTPHeaderField: "Cookie") == "auth=test") #expect(billingRequest.value(forHTTPHeaderField: "Referer") == "https://opencode.ai/workspace/wrk_TEST123") } @Test func `optional zen balance helper uses normalized cookie and workspace override`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } var observedCookie: String? OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } observedCookie = request.value(forHTTPHeaderField: "Cookie") #expect(url.path == "/workspace/wrk_TEST123") return Self.makeResponse( url: url, body: #"

現在の残高 $98.76

"#, statusCode: 200, contentType: "text/html") } let balance = try await OpenCodeGoUsageFetcher.fetchOptionalZenBalance( cookieHeader: "provider=google; auth=test", timeout: 2, workspaceIDOverride: "https://opencode.ai/workspace/wrk_TEST123/go", session: self.makeSession()) #expect(balance == 98.76) #expect(observedCookie == "auth=test") } @Test func `optional zen balance failure does not fail subscription usage`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } var rootTimeout: TimeInterval? OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } if url.path == "/workspace/wrk_TEST123" { rootTimeout = request.timeoutInterval throw URLError(.timedOut) } return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 60, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) #expect(snapshot.rollingUsagePercent == 17) #expect(snapshot.zenBalanceUSD == nil) #expect(rootTimeout == 60) } @Test func `optional zen balance does not stall subscription usage`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } if url.path == "/workspace/wrk_TEST123" { Thread.sleep(forTimeInterval: 1) return Self.makeResponse( url: url, body: #"

現在の残高 $98.76

"#, statusCode: 200, contentType: "text/html") } return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } let start = ContinuousClock.now let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 60, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) let elapsed = start.duration(to: ContinuousClock.now) #expect(snapshot.rollingUsagePercent == 17) #expect(snapshot.zenBalanceUSD == nil) #expect(elapsed < .milliseconds(700)) } @Test func `optional zen balance can be skipped by settings`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } var observedPaths: [String] = [] OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } observedPaths.append(url.path) return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } let snapshot = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 60, workspaceIDOverride: "wrk_TEST123", includeZenBalance: false, session: self.makeSession()) #expect(snapshot.rollingUsagePercent == 17) #expect(snapshot.zenBalanceUSD == nil) #expect(observedPaths == ["/workspace/wrk_TEST123/go"]) } @Test func `optional zen balance cancellation propagates`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } let rootStarted = AsyncStream.makeStream(of: Void.self) OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } if url.path == "/workspace/wrk_TEST123" { rootStarted.continuation.yield(()) Thread.sleep(forTimeInterval: 0.2) return Self.makeResponse( url: url, body: #"

現在の残高 $98.76

"#, statusCode: 200, contentType: "text/html") } return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } let task = Task { try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "auth=test", timeout: 60, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) } let started = await withTaskGroup(of: Bool.self) { group in group.addTask { var iterator = rootStarted.stream.makeAsyncIterator() return await iterator.next() != nil } group.addTask { try? await Task.sleep(for: .seconds(2)) return false } let result = await group.next() ?? false group.cancelAll() return result } #expect(started) task.cancel() do { _ = try await task.value Issue.record("Expected cancellation to propagate.") } catch is CancellationError { // Expected. } catch { Issue.record("Expected CancellationError, got: \(error)") } } @Test func `fetcher sends only auth cookie to opencode host`() async throws { defer { OpenCodeGoStubURLProtocol.handler = nil } var observedCookie: String? OpenCodeGoStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } observedCookie = request.value(forHTTPHeaderField: "Cookie") return Self.makeResponse( url: url, body: Self.goUsagePageHTML( workspaceID: "wrk_TEST123", rolling: UsageWindow(percent: 17, resetInSec: 600), weekly: UsageWindow(percent: 75, resetInSec: 7200), monthly: nil), statusCode: 200, contentType: "text/html") } _ = try await OpenCodeGoUsageFetcher.fetchUsage( cookieHeader: "provider=google; auth=test", timeout: 2, workspaceIDOverride: "wrk_TEST123", session: self.makeSession()) #expect(observedCookie == "auth=test") } private static func goUsagePageHTML( workspaceID: String, rolling: UsageWindow, weekly: UsageWindow, monthly: UsageWindow?) -> String { let monthlyField: String? = if let monthly { #"monthlyUsage:{status:"ok",resetInSec:\#(monthly.resetInSec),usagePercent:\#(monthly.percent)}"# } else { nil } let usageFields = [ #"rollingUsage:{status:"ok",resetInSec:\#(rolling.resetInSec),usagePercent:\#(rolling.percent)}"#, #"weeklyUsage:{status:"ok",resetInSec:\#(weekly.resetInSec),usagePercent:\#(weekly.percent)}"#, monthlyField, ] .compactMap(\.self) .joined(separator: ",") return """ """ } private static func makeResponse( url: URL, body: String, statusCode: Int, contentType: String) -> (HTTPURLResponse, Data) { let response = HTTPURLResponse( url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": contentType])! return (response, Data(body.utf8)) } } final class OpenCodeGoStubURLProtocol: URLProtocol { nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override static func canInit(with request: URLRequest) -> Bool { request.url?.host == "opencode.ai" } override static func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { guard let handler = Self.handler else { self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse)) return } do { let (response, data) = try handler(self.request) self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) self.client?.urlProtocol(self, didLoad: data) self.client?.urlProtocolDidFinishLoading(self) } catch { self.client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} }