Swift 3 custom URLProtocol crashes when converting Error to NSError - swift

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...

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);
}
}
...
}

Related

How to cancel an `async` function with cancellable type returned from `async` operation initiation

I need to support cancellation of a function that returns an object that can be cancelled after initiation. In my case, the requester class is in a 3rd party library that I can't modify.
actor MyActor {
...
func doSomething() async throws -> ResultData {
var requestHandle: Handle?
return try await withTaskCancellationHandler {
requestHandle?.cancel() // COMPILE ERROR: "Reference to captured var 'requestHandle' in concurrently-executing code"
} operation: {
return try await withCheckedThrowingContinuation{ continuation in
requestHandle = requester.start() { result, error in
if let error = error
continuation.resume(throwing: error)
} else {
let myResultData = ResultData(result)
continuation.resume(returning: myResultData)
}
}
}
}
}
...
}
I have reviewed other SO questions and this thread: https://forums.swift.org/t/how-to-use-withtaskcancellationhandler-properly/54341/4
There are cases that are very similar, but not quite the same. This code won't compile because of this error:
"Reference to captured var 'requestHandle' in concurrently-executing code"
I assume the compiler is trying to protect me from using the requestHandle before it's initialized. But I'm not sure how else to work around this problem. The other examples shown in the Swift Forum discussion thread all seem to have a pattern where the requester object can be initialized before calling its start function.
I also tried to save the requestHandle as a class variable, but I got a different compile error at the same location:
Actor-isolated property 'profileHandle' can not be referenced from a
Sendable closure
You said:
I assume the compiler is trying to protect me from using the requestHandle before it’s initialized.
Or, more accurately, it is simply protecting you against a race. You need to synchronize your interaction with your “requester” and that Handle.
But I’m not sure how else to work around this problem. The other examples shown in the Swift Forum discussion thread all seem to have a pattern where the requester object can be initialized before calling its start function.
Yes, that is precisely what you should do. Unfortunately, you haven’t shared where your requester is being initialized or how it was implemented, so it is hard for us to comment on your particular situation.
But the fundamental issue is that you need to synchronize your start and cancel. So if your requester doesn’t already do that, you should wrap it in an object that provides that thread-safe interaction. The standard way to do that in Swift concurrency is with an actor.
For example, let us imagine that you are wrapping a network request. To synchronize your access with this, you can create an actor:
actor ResponseDataRequest {
private var handle: Handle?
func start(completion: #Sendable #escaping (Data?, Error?) -> Void) {
// start it and save handle for cancelation, e.g.,
handle = requestor.start(...)
}
func cancel() {
handle?.cancel()
}
}
That wraps the starting and canceling of a network request in an actor. Then you can do things like:
func doSomething() async throws -> ResultData {
let responseDataRequest = ResponseDataRequest()
return try await withTaskCancellationHandler {
Task { await responseDataRequest.cancel() }
} operation: {
return try await withCheckedThrowingContinuation { continuation in
Task {
await responseDataRequest.start { result, error in
if let error = error {
continuation.resume(throwing: error)
} else {
let resultData = ResultData(result)
continuation.resume(returning: resultData)
}
}
}
}
}
}
You obviously can shift to unsafe continuations when you have verified that everything is working with your checked continuations.
After reviewing the Swift discussion thread again, I see you can do this:
...
var requestHandle: Handle?
let onCancel = { profileHandle?.cancel() }
return try await withTaskCancellationHandler {
onCancel()
}
...

Handling Errors with intent parameters and Dynamic options with Siri

I've an intent parameter set as dynamic from the intent definition.
Let's say that the server where I get information for this option is currently down.
It is not clear how to present to users the fact that the options at the moment cannot be retrieved. The completion field where we should return the options also accepts an Error.
I've filled it with a subclass of Error and I've also implemented the LocalizedError protocol for this class... but when I encounter the error from the shortcut App, Apple is just presenting a pop up message that returns a terrible message not localized (but that includes the right Error name).
Here is the code that I'm using...
func provideCarModelOptions(for intent: CarIntent, with completion: #escaping ([String]?, Error?) -> Void) {
if(somethingGoesWrongWithServers()){
completion([],CarError.ServerDown)
}else{
completion(ReturnListOfModels(), nil)
}
}
And this is how I've implementend the CarError enum
public enum CarError:Error{
case serverDown
case generic
}
extension CarError : LocalizedError{
public var errorDescription: String? {
switch self {
case .serverDown:
return "Server is down"
case .generic:
return "SomethingGoesWrong"
}
}
}
Am I doing anything wrong or Apple is not handling the Errors the right way?
This worked for me to provide localized description:
completion(nil, INIntentError.init(_nsError: NSError(domain: "com.Domain.error", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error Message"])))

Customizing sandboxed NSSavePanel alert

I am validating urls from NSSavePanel using the delegate's panel(_:validate) method, throwing error in case of invalid url. In such case the NSSavePanel presents an alert, which I want to customize (meaning present some human readable description) depending on the error thrown, keeping the save panel window open and then letting you choose another path.
LocalizedError works just fine when not using App Sandbox but in a sandboxed app the getter for error description is never called and the message in the alert is generic "Operation couldn't be completed. (#yourErrorType)", which I guess is somehow caused by the different inheritance chain for sandboxed NSSavePanels.
I am struggling figuring a way around this - is it possible to customize the alert somehow while still keeping the app sandboxed?
Addendum: Permissions for User Selected File => r/w. Running the following example produces different alerts with/without sandbox.
func runSavePanel()
{
let panel = NSSavePanel()
let delegate = SavePanelDelegate()
panel.delegate = delegate
_ = panel.runModal()
}
class SavePanelDelegate: NSObject, NSOpenSavePanelDelegate {
func panel(_ sender: Any, validate url: URL) throws {
throw CustomError.whatever
}
}
enum CustomError: LocalizedError {
case whatever
var errorDescription: String? {
get {
return "my description"
}
}
}
So, after a bit of further digging I can tell the solution of the riddle finally although I can only guess the reasons why it was made tricky by Apple. Apparently NSError exclusively needs to be used. The customization has to be done in userInfo, say
let userInfo = [NSLocalizedDescriptionKey: "yourLocalizedDescription", NSLocalizedRecoverySuggestionErrorKey: "yourSuggestion"]
throw NSError(domain: "whatever", code: 0, userInfo: userInfo)
etc. By the way subclassing NSError doesn't work, the Sandbox will just happily ignore you :)

Adopting CustomNSError in DecodingError

I'm writing an error logger using Crashlytics and I've come up against an issue that is making me question my understanding of protocols and dynamic dispatch.
When recording non fatal errors using Crashlytics the API expects an Error conforming object, and an optional user info dictionary. I'm looking at JSON decoding errors at the moment, and I wasn't too happy with what I was seeing in the Crashlytics dashboard when I just sent the DecodingError along in recordError. So my solution was to write an extension for DecodingError adopting CustomNSError to provide some more verbose info to help with debugging in the future:
extension DecodingError: CustomNSError {
public static var errorDomain: String {
return "com.domain.App.ErrorDomain.DecodingError"
}
public var errorCode: Int {
switch self {
case .dataCorrupted:
return 1
case .keyNotFound:
return 2
case .typeMismatch:
return 3
case .valueNotFound:
return 4
}
}
public var errorUserInfo: [String : Any] {
switch self {
case .dataCorrupted(let context):
var userInfo: [String: Any] = [
"debugDescription": context.debugDescription,
"codingPath": context.codingPath.map { $0.stringValue }.joined(separator: ".")
]
guard let underlyingError = context.underlyingError else { return userInfo }
userInfo["underlyingErrorLocalizedDescription"] = underlyingError.localizedDescription
userInfo["underlyingErrorDebugDescription"] = (underlyingError as NSError).debugDescription
userInfo["underlyingErrorUserInfo"] = (underlyingError as NSError).userInfo.map {
return "\($0.key): \(String(describing: $0.value))"
}.joined(separator: ", ")
return userInfo
case .keyNotFound(let codingKey, let context):
return [
"debugDescription": context.debugDescription,
"codingPath": context.codingPath.map { $0.stringValue }.joined(separator: "."),
"codingKey": codingKey.stringValue
]
case .typeMismatch(_, let context), .valueNotFound(_, let context):
return [
"debugDescription": context.debugDescription,
"codingPath": context.codingPath.map { $0.stringValue }.joined(separator: ".")
]
}
}
}
I've written a method in my logger which looks like this:
func log(_ error: CustomNSError) {
Crashlytics.sharedInstance().recordError(error)
}
And I send the error along here:
do {
let decoder = JSONDecoder()
let test = try decoder.decode(SomeObject.self, from: someShitJSON)
} catch(let error as DecodingError) {
switch error {
case .dataCorrupted(let context):
ErrorLogger.sharedInstance.log(error)
default:
break
}
}
But the object that gets passed to the log(_ error:) is not my implementation of CustomNSError, looks like a standard NSError with the NSCocoaErrorDomain.
I hope that's detailed enough to explain what I mean, not sure why the object being passed to log doesn't have the values I set up in the extension to DecodingError. I know I could easily just send across the additional user info separately in my call to Crashlytics, but I'd quite like to know where I'm going wrong with my understanding of this scenario.
NSError bridging is an interesting beast in the Swift compiler. On the one hand, NSError comes from the Foundation framework, which your application may or may not use; on the other, the actual bridging mechanics need to be performed in the compiler, and rightfully, the compiler should have as little knowledge of "high-level" libraries above the standard library as possible.
As such, the compiler has very little knowledge of what NSError actually is, and instead, Error exposes three properties which provide the entirety of the underlying representation of NSError:
public protocol Error {
var _domain: String { get }
var _code: Int { get }
// Note: _userInfo is always an NSDictionary, but we cannot use that type here
// because the standard library cannot depend on Foundation. However, the
// underscore implies that we control all implementations of this requirement.
var _userInfo: AnyObject? { get }
// ...
}
NSError, then, has a Swift extension which conforms to Error and implements those three properties:
extension NSError : Error {
#nonobjc
public var _domain: String { return domain }
#nonobjc
public var _code: Int { return code }
#nonobjc
public var _userInfo: AnyObject? { return userInfo as NSDictionary }
// ...
}
With this, when you import Foundation, any Error can be cast to an NSError and vice versa, as both expose _domain, _code, and _userInfo (which is what the compiler actually uses to perform the bridging).
The CustomNSError protocol plays into this by allowing you to supply an errorDomain, errorCode, and errorUserInfo, which are then exposed by various extensions as their underscore versions:
public extension Error where Self : CustomNSError {
/// Default implementation for customized NSErrors.
var _domain: String { return Self.errorDomain }
/// Default implementation for customized NSErrors.
var _code: Int { return self.errorCode }
// ...
}
So, how are EncodingError and DecodingError different? Well, since they're both defined in the standard library (which is present regardless of whether or not you use Foundation, and cannot depend on Foundation), they hook into the system by providing implementations of _domain, _code, and _userInfo directly.
Since both types provide the direct underscore versions of those variables, they don't call in to the non-underscore versions to get the domain, code, and user info — the values are used directly (rather than rely on var _domain: String { return Self.errorDomain }).
So, in effect, you can't override the behavior because EncodingError and DecodingError already provide this info. Instead, if you want to provide different codes/domains/user info dictionaries, you're going to need to write a function which takes an EncodingError/DecodingError and returns your own NSError, or similar.

Swift do-try-catch syntax

I give it a try to understand new error handling thing in swift 2. Here is what I did: I first declared an error enum:
enum SandwichError: ErrorType {
case NotMe
case DoItYourself
}
And then I declared a method that throws an error (not an exception folks. It is an error.). Here is that method:
func makeMeSandwich(names: [String: String]) throws -> String {
guard let sandwich = names["sandwich"] else {
throw SandwichError.NotMe
}
return sandwich
}
The problem is from the calling side. Here is the code that calls this method:
let kitchen = ["sandwich": "ready", "breakfeast": "not ready"]
do {
let sandwich = try makeMeSandwich(kitchen)
print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
print("Not me error")
} catch SandwichError.DoItYourself {
print("do it error")
}
After the do line compiler says Errors thrown from here are not handled because the enclosing catch is not exhaustive. But in my opinion it is exhaustive because there is only two case in SandwichError enum.
For regular switch statements swift can understands it is exhaustive when every case handled.
There are two important points to the Swift 2 error handling model: exhaustiveness and resiliency. Together, they boil down to your do/catch statement needing to catch every possible error, not just the ones you know you can throw.
Notice that you don't declare what types of errors a function can throw, only whether it throws at all. It's a zero-one-infinity sort of problem: as someone defining a function for others (including your future self) to use, you don't want to have to make every client of your function adapt to every change in the implementation of your function, including what errors it can throw. You want code that calls your function to be resilient to such change.
Because your function can't say what kind of errors it throws (or might throw in the future), the catch blocks that catch it errors don't know what types of errors it might throw. So, in addition to handling the error types you know about, you need to handle the ones you don't with a universal catch statement -- that way if your function changes the set of errors it throws in the future, callers will still catch its errors.
do {
let sandwich = try makeMeSandwich(kitchen)
print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
print("Not me error")
} catch SandwichError.DoItYourself {
print("do it error")
} catch let error {
print(error.localizedDescription)
}
But let's not stop there. Think about this resilience idea some more. The way you've designed your sandwich, you have to describe errors in every place where you use them. That means that whenever you change the set of error cases, you have to change every place that uses them... not very fun.
The idea behind defining your own error types is to let you centralize things like that. You could define a description method for your errors:
extension SandwichError: CustomStringConvertible {
var description: String {
switch self {
case NotMe: return "Not me error"
case DoItYourself: return "Try sudo"
}
}
}
And then your error handling code can ask your error type to describe itself -- now every place where you handle errors can use the same code, and handle possible future error cases, too.
do {
let sandwich = try makeMeSandwich(kitchen)
print("i eat it \(sandwich)")
} catch let error as SandwichError {
print(error.description)
} catch {
print("i dunno")
}
This also paves the way for error types (or extensions on them) to support other ways of reporting errors -- for example, you could have an extension on your error type that knows how to present a UIAlertController for reporting the error to an iOS user.
I suspect this just hasn’t been implemented properly yet. The Swift Programming Guide definitely seems to imply that the compiler can infer exhaustive matches 'like a switch statement'. It doesn’t make any mention of needing a general catch in order to be exhaustive.
You'll also notice that the error is on the try line, not the end of the block, i.e. at some point the compiler will be able to pinpoint which try statement in the block has unhandled exception types.
The documentation is a bit ambiguous though. I’ve skimmed through the ‘What’s new in Swift’ video and couldn’t find any clues; I’ll keep trying.
Update:
We’re now up to Beta 3 with no hint of ErrorType inference. I now believe if this was ever planned (and I still think it was at some point), the dynamic dispatch on protocol extensions probably killed it off.
Beta 4 Update:
Xcode 7b4 added doc comment support for Throws:, which “should be used to document what errors can be thrown and why”. I guess this at least provides some mechanism to communicate errors to API consumers. Who needs a type system when you have documentation!
Another update:
After spending some time hoping for automatic ErrorType inference, and working out what the limitations would be of that model, I’ve changed my mind - this is what I hope Apple implements instead. Essentially:
// allow us to do this:
func myFunction() throws -> Int
// or this:
func myFunction() throws CustomError -> Int
// but not this:
func myFunction() throws CustomErrorOne, CustomErrorTwo -> Int
Yet Another Update
Apple’s error handling rationale is now available here. There have also been some interesting discussions on the swift-evolution mailing list. Essentially, John McCall is opposed to typed errors because he believes most libraries will end up including a generic error case anyway, and that typed errors are unlikely to add much to the code apart from boilerplate (he used the term 'aspirational bluff'). Chris Lattner said he’s open to typed errors in Swift 3 if it can work with the resilience model.
Swift is worry that your case statement is not covering all cases, to fix it you need to create a default case:
do {
let sandwich = try makeMeSandwich(kitchen)
print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
print("Not me error")
} catch SandwichError.DoItYourself {
print("do it error")
} catch Default {
print("Another Error")
}
I was also disappointed by the lack of type a function can throw, but I get it now thanks to #rickster and I'll summarize it like this: let's say we could specify the type a function throws, we would have something like this:
enum MyError: ErrorType { case ErrorA, ErrorB }
func myFunctionThatThrows() throws MyError { ...throw .ErrorA...throw .ErrorB... }
do {
try myFunctionThatThrows()
}
case .ErrorA { ... }
case .ErrorB { ... }
The problem is that even if we don't change anything in myFunctionThatThrows, if we just add an error case to MyError:
enum MyError: ErrorType { case ErrorA, ErrorB, ErrorC }
we are screwed because our do/try/catch is no longer exhaustive, as well as any other place where we called functions that throw MyError
enum NumberError: Error {
case NegativeNumber(number: Int)
case ZeroNumber
case OddNumber(number: Int)
}
extension NumberError: CustomStringConvertible {
var description: String {
switch self {
case .NegativeNumber(let number):
return "Negative number \(number) is Passed."
case .OddNumber(let number):
return "Odd number \(number) is Passed."
case .ZeroNumber:
return "Zero is Passed."
}
}
}
func validateEvenNumber(_ number: Int) throws ->Int {
if number == 0 {
throw NumberError.ZeroNumber
} else if number < 0 {
throw NumberError.NegativeNumber(number: number)
} else if number % 2 == 1 {
throw NumberError.OddNumber(number: number)
}
return number
}
Now Validate Number :
do {
let number = try validateEvenNumber(0)
print("Valid Even Number: \(number)")
} catch let error as NumberError {
print(error.description)
}
Error can be handle using switch case in catch
func checkAge(age:Int) throws {
guard !(age>0 && age < 18) else{
throw Adult.child
}
guard !(age >= 60) else{
throw Adult.old
}
guard (age>0) else{
throw Adult.notExist
}
}
do{
try checkAge(age:0)
}
catch let error {
switch error{
case Adult.child : print("child")
case Adult.old : print("old")
case Adult.notExist : print("not Exist")
default:
print("default")
}
}
enum Adult:Error {
case child
case old
case notExist
}
Create enum like this:
//Error Handling in swift
enum spendingError : Error{
case minus
case limit
}
Create method like:
func calculateSpending(morningSpending:Double,eveningSpending:Double) throws ->Double{
if morningSpending < 0 || eveningSpending < 0{
throw spendingError.minus
}
if (morningSpending + eveningSpending) > 100{
throw spendingError.limit
}
return morningSpending + eveningSpending
}
Now check error is there or not and handle it:
do{
try calculateSpending(morningSpending: 60, eveningSpending: 50)
} catch spendingError.minus{
print("This is not possible...")
} catch spendingError.limit{
print("Limit reached...")
}