Swift 3 custom URLProtocol crashes when converting

2019-08-19 07:56发布

问题:

I've got a rather large body of Swift 3 code for Mac OS 10.11 and up (using Xcode 8.2.1). There are a number of processes, among them a GUI application and a background service. Both of these use a custom URLProtocol, which is implemented in a framework (imported by both application and service). The protocol sometimes may generate instances of an enum that conforms to Error, which it catches and handles appropriately (generally by using the URLProtocolClient to toss them up to the URLSession trying to make the request).

  • When there's no error, both the app and the service work fine.
  • When there is an error, the app works fine (well, as expected).
  • When there is an error, the service crashes.

Wandering through the debugger has shown that this crash is occurring when the Error is automatically converted into an NSError by the runtime. I added this cast explicitly in my code, and sure enough, I get the same crash, now on that line.

I saw Swift 3.1: Crash when custom error is converted to NSError to access its domain property, but it doesn't apply here:

  • The solution there - extending the Error to a CustomNSError (and LocalizedError for good measure) and implementing the relevant properties - didn't help.
  • The crash occurs after the domain has been obtained; as far as I can tell that was not a problem for me.
  • Possibly relevant: that was on iOS, not Mac.

Having already tried the only listed solution to that question, I'm at something of a loss. I've been debugging this for hours without getting anywhere except that it seems to happen somewhere deep in the guts of dyld.

Code:

enum CrowbarProtocolError: Error {
    case FailedToCreateSocket;
    case PathTooLong;
    case NonSocketFile;
    case SocketNotFound;
...
    case UnrecognizedError(errno: Int32);
}
extension CrowbarProtocolError: LocalizedError {
    public var errorDescription: String? {
        return "Some localized description"
    }
}
extension CrowbarProtocolError: CustomNSError {
    public static var errorDomain: String {
        return "Some Domain Name"
    }
    public var errorCode: Int {
        return 204 //Should be your custom error code.
    }
    public var errorUserInfo: [String: Any] {
        return ["WTF": "Swift?"];
    }
}
...
open class CrowbarUrlProtocol: URLProtocol  {
...
    override open func startLoading() {
        do {
            let sockHandle = try CrowbarUrlProtocol.openSocket();
            let req = try buildRequestData();
            sockHandle.write(req);
            NotificationCenter.default.addObserver(
                self, 
                selector: #selector(self.readCompleted), 
                name: .NSFileHandleReadToEndOfFileCompletion, 
                object: nil);
            sockHandle.readToEndOfFileInBackgroundAndNotify();
        } catch let err {
            Log.warn("CrowbarUrlProtocol request failed with error \(err)");
            // -- In the background service, THIS LINE CRASHES! --
            let err2 = err as NSError;
            Log.warn("As NSError: \(err2)");
            // -- Without the explicit cast, it crashes on this line --
            self.client?.urlProtocol(self, didFailWithError: err);
        }
    }
...
}

One idea I have for solving this is just doing everything (or, as much as possible) using NSErrors, on the grounds that if there's never a need to convert an Error to an NSError, then whatever is causing this crash won't happen. No idea if it'll work but it seems worth a try...

回答1:

OK, as far as I can tell this is just a bug in Swift's runtime, but I found a work-around: just use NSError for everything involving Obj-C code rather than Swift Errors (to avoid the implicit cast). Since I already was implementing CustomNSError, it was easy to just create a toNSError() function on my Error enum, and use that for the self.client?.urlProtocol(self, didFailWithError: err) lines.

enum CrowbarProtocolError: Error, CustomNSError {
    case FailedToCreateSocket;
    case PathTooLong;
...
    public func asNSError() -> NSError {
        return NSError(domain: CrowbarProtocolError.errorDomain,
                    code: self.errorCode,
                    userInfo: self.errorUserInfo);
    }
}
...
open class CrowbarUrlProtocol: URLProtocol  {
...
    override open func startLoading() {
        do {
            let sockHandle = try CrowbarUrlProtocol.openSocket();
            let req = try buildRequestData();
            sockHandle.write(req);
            NotificationCenter.default.addObserver(
                self, 
                selector: #selector(self.readCompleted), 
                name: .NSFileHandleReadToEndOfFileCompletion, 
                object: nil);
            sockHandle.readToEndOfFileInBackgroundAndNotify();
        } catch let err as CrowbarProtocolError {
            Log.warn("CrowbarUrlProtocol request failed with error \(err)");
            self.client?.urlProtocol(self, didFailWithError: err.asNSError());
        }
        catch let err {
            Log.warn("CrowbarUrlProtocol caught non-CrowbarProtocol Error \(err)");
            // This would probably crash the service, but shouldn't ever happen
            self.client?.urlProtocol(self, didFailWithError: err);
        }
    }
...
}