Cannot send URLProtocol subclass across actor boundary

Hello fellow nerds,

I came across this very annoying compilation error after upgrading to Xcode 26.0 and Swift 6.2.

Say you have this actor:

import Foundation

@globalActor
actor MyActor: GlobalActor {
    static let shared: MyActor = .init()
}

Now say you want to implement a subclass of URLProtocol just to mock some network calls.

Example 1:

final class NoSendableURLProtocol: URLProtocol {
    final override func startLoading() {
        Task { @MyActor [weak self] in
            guard let self else { return } // error: Sending 'self' risks causing data races
        }
    }
}

Example 2:

@MyActor
final class ActorURLProtocol: URLProtocol {
    final override func startLoading() {
        Task { @MyActor [weak self] in
            guard let self else { return } // error: Sending 'self' risks causing data races
        }
    }
}

Example 3:

@preconcurrency
final class PreconcurrencyURLProtocol: URLProtocol {
    final override func startLoading() {
        Task { @MyActor [weak self] in
            guard let self else { return } // error: Sending 'self' risks causing data races
        }
    }
}

Example 4:

final class SendableURLProtocol: URLProtocol, Sendable { // 'Sendable' class 'SendableURLProtocol' cannot inherit from another class other than 'NSObject'
    final override func startLoading() {
        Task { @MyActor [weak self] in
            guard let self else { return } // error: same as before
        }
    }
}

Example 5:

@preconcurrency
final class UncheckedSendableURLProtocol: URLProtocol, @unchecked Sendable { // Conformance of 'UncheckedSendableURLProtocol' to protocol 'Sendable' is already unavailable
    final override func startLoading() {
        Task { @MyActor [weak self] in
            guard let self else { return } // error: same as before
        }
    }
}

Example 6:

final class WorkingURLProtocol: URLProtocol, @unchecked Sendable {
    final override func startLoading() {
        Task { @MyActor [request = task?.originalRequest, requestURL = task?.originalRequest?.url, client] in
            guard let request, let requestURL else {
                fatalError()
            }

            defer {
                client?.urlProtocolDidFinishLoading(self)
            }

            // A whole lot of code, so that the compiler "forgets" to check that `self` is being sent

            client?.urlProtocol(self, didLoad: .init())
        }
    }
}

In a Playground, I can't get this to work actually, but I can in a project of mine. Can't share the code unfortunately, but I guarantee you the compiler "stops caring" at one point.


Example #4 above led me to this post, which doesn’t really address the issue.

It seems to me that subclasses of NSObject that are themselves not final cause the sendable checks to go nuts. So a subclass of a subclass (ie. MyProtocol: URLProtocol: NSObject) does not play well with Sendable.

TLDR

What is a bug in my opinion though, is that marking it as @unchecked Sendable does nothing, neither does @preconcurrency.


Extra bits:

  • Xcode Version 26.0 (17A324)
  • swift-driver version: 1.127.14.1 Apple Swift version 6.2 (swiftlang-6.2.0.19.9 clang-1700.3.19.1)

I believe the root cause of this bug is hinted at by this error message:

The error message notes that the Sendable conformance is already unavailable, and I believe that’s coming from the imported URLProtocol type itself. On Darwin it’s marked as explicitly unavailable by nature of it being in a NS_HEADER_AUDIT_BEGIN(sendability) block and on Linux/Windows the conformance is explicitly marked as unavailable in the Swift source. I will need to check, but I think this is incorrect - I think that URLProtocol should be neither explicitly Sendable nor non-Sendable and should instead leave its Sendable conformance up to subclasses (I’ll need to double check on whether that’s indeed the case or if URLProtocol has some part of it that prevents Sendable conformances even in subclasses). The already-existing unavailable Sendable conformance is why you can’t add a Sendable conformance of your own.

cc @hborla - I believe this is the same category as the DateFormatter Sendable conformance where we don’t yet have a great way to import an Objective-C type as “potentially Sendable".

2 Likes