Closed groue closed 1 year ago
Looking at this, it appears this is coming from the transport-agnostic UniversalClient
. As you note all the errors are wrapped.
I'm wondering if we removed one layer of wrapping here^1 then the underlying error would be the URLError, rather than a OpenAPIRuntime.RuntimeError.
@groue is the principal complaint that we taint the localised description or that you cannot get your hands on the underlying error, or both?
So, if we remove that layer, like so...
--- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift
+++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift
@@ -130,7 +130,7 @@ public struct UniversalClient: Sendable {
operationID: operationID
)
} mapError: { error in
- RuntimeError.transportFailed(error)
+ error
}
}
for middleware in middlewares.reversed() {
...then we are able to do this...
struct MockClientTransport: ClientTransport {
func send(_ request: Request, baseURL: URL, operationID: String) async throws -> Response {
throw URLError(.notConnectedToInternet)
}
}
let client = Client(serverURL: URL(string: "not used")!, transport: MockClientTransport())
do {
_ = try await client.getGreeting(.init())
} catch let error as ClientError {
if let urlError = error.underlyingError as? URLError {
print(urlError.code)
print(urlError.localizedDescription)
}
}
We'd need to think through the implications of this change, but presumably this is the concrete ask @groue?
I wonder if ClientError should have both underlyingError
and some kind of causeDescription
where we could put e.g. "transportFailed". It's useful information but I agree it probably shouldn't make it impossible to get the original thrown error.
Hello @simonjbeaumont, @czechboy0
The main issue is the impossibility to access the raw URLError
, yes.
Please excuse me for talking about the "message corruption" in the initial issue. This only happens in internal apis, and I could only observe it indirectly, by printing the error. I should not have presented this as a problem.
It's difficult to me to suggest a refactoring, because I'm not familiar with OpenAPI error types and cases, and their respective goals. In particular, I'm unsure which cases are intended for debugging feedback (in which case the api contract should be weak), and which errors are expected to be used for control flow in the client app (in which case the api contract must be very strong).
I do intend to use the URLError
for control flow, and that's why I need a strong api contract that allows me to get it (when it exists).
What I can note, though, is that the ClientError
I got had a underlyingError
of type RuntimeError
. Since RuntimeError
is internal, it is only useful via the public protocol it conforms to: LocalizedError
and CustomStringConvertible
. So basically it only provides some strings, and is thus unsuitable for control flow.
So... I can finally share an idea with you π
Please update the documentation of ClientError.underlyingError
so that it specifies that errors thrown by the Client's transport, if any, are directly returned by this property, without exception.
That would become the strong contract on which client apps could rely on, in order to write control flow based on ClientError
. Some tests would enforce this contract in the long run.
// One can't be more straightforward
let clientError: ClientError
// Thanks to the new documentation of ClientError.underlyingError,
// I'm 100% guaranteed that this is the way to get the URLError
// thrown by URLSession, and perform reliable control flow.
if let urlError = clientError.underlyingError as? URLError {
// Perform control flow with the raw URLError
} else {
// Other errors thrown by URLSessionTransport,
// or by OpenAPI.
}
Disclaimer: I'm not familiar with the error system of the library, so it's likely I forget other important use cases when I'm focusing on my own π It's a frequent bias of new users π¬
Note: It is in the pitch for typed throws in the Swift forums that I learned about errors used for "debugging" (weak api contract) vs. errors used for "control flow" (strong api contract, with respect for source stability in the client applications, and totally in the realm of semantic versioning). I found these concepts useful, that's how I could suggest a documentation update, above. And of course the main course about the philosophy of errors is the enlightening Precise error typing in Swift.
EDIT: If my suggestion for updating the documentation of ClientError.underlyingError
sounds OK to you, we'd also need to update the documentation (and maybe the runtime) of URLSessionTransport
, so that it guarantees that all URLError
are rethrown, unmodified, from send(_:baseURL:operationID:)
.
A possible alternative would be that URLSessionTransport
is documented to throw a specific error (let's say URLSessionTransportError
) that gives access to the raw URLError
(but we'd need to identify (current or future) use cases that URLSessionTransport
would like to allow with this extra error type):
let clientError: ClientError
// Fine as well, as long as there exists a documented contract
// enforced by tests in the OpenAPI libraries.
if let transportError = clientError.underlyingError as? URLSessionTransportError,
let urlError = transportError.underlyingError as? URLError
{
// Perform control flow with the raw URLError
} else {
// Other errors thrown by URLSessionTransport,
// or by OpenAPI.
}
Thanks for taking the time to crystallise this @groue.
I think it's a reasonable ask that the ClientError.underlyingError
be usable for control flow. I think removing the additional layer of internal OpenAPIRuntime.RuntimeError
in OpenAPIRuntime.UniversalClient
can help move this forward.
I think it's also reasonable for the documentation for ClientTransport
encourage transport implementations to provide the underlying error in a way that can be used for control flow (cf. there being any such internal wrapping within the transport).
Of course, this project cannot make any guarantee that the transport implementation follows this recommendation so adopters would then be in the hands of those packages. The only API contract that the OpenAPIRuntime package could make is to faithfully propagate the underlying error from the transport, which may or may not be useful.
In the case of the URLSessionTransport, we can make that happen, of course.
I haven't formed a strong opinion on whether URLSessionTransport
should wrap the error in a public URLSessionTransportError
or just push through the URLError
, although I'm leaning toward the latter.
What do you think to all this @czechboy0?
Thanks for your feedback, @simonjbeaumont. Let's wait for @czechboy0.
Meanwhile:
Of course, this project cannot make any guarantee that the transport implementation follows this recommendation so adopters would then be in the hands of those packages.
I agree. Yet we have a way to strongly encourage transport implementers to be virtuous, with yet another documentation change, in ClientTransport.send(_:body:baseURL:operationID:)
:
/// Sends the underlying HTTP request and returns the received HTTP response.
+///
+/// Errors thrown by this method are rethrown, unmodified, by the
+/// `Client` types generated by the Swift OpenAPI Generator. It is
+/// strongly recommended that your implementation throws public
+/// and well-documented errors. This will help developers who use
+/// generated clients perform reliable error handling.
The Implement a custom client transport section could contain a similar note as well.
We can never force people to implement protocol semantics. But we can document them, explain the intent behind them, and reveal what can turn wrong if they are not honored.
For example, we can not force people to implement matching implementations for Hashable
and Equatable
. Yet the Hashable
documentation uses the word must:
Two instances that are equal must feed the same values to Hasher in
hash(into:)
, in the same order.
I agree we should keep the original thrown error in underlyingError, but also add a causeDescription, because the wrapping today does add information that we don't want to lose, which explains what was happening when the underlying error was thrown.
Implementation-wise, I suspect we could move RuntimeError's description to be returned by ClientError's hypothetical new causeDescription, and keep underlyingError reserved for errors thrown by subsystems. That means it might be nil in many cases, for example when there's a missing header, the ClientError would only have causeDescription with that information, and a nil underlyingError.
Since this is practically a breaking change, we should align it with a minor bump pre-1.0, possibly even with 0.3.0 going out next week, if the change can be done by early next week.
Since this is practically a breaking change, we should align it with a minor bump pre-1.0, possibly even with 0.3.0 going out next week, if the change can be done by early next week.
How is it a breaking change?
The new causeDescription
would be an API-additive change in OpenAPIRuntime, along with some documentation changes. The URLSessionTransport change would go from providing an error that cannot be matched at all to one that canβisn't that ostensibly API-additive too?
Yes, but for many other cases, underlyingError would become nil, where it was not nil before (since people should move to causeDescription to understand the context of what threw the underlying error).
OpenAPIRuntime makes it impossible to access precise error localized message and codes.
To reproduce:
Client
with aURLSessionTransport
Expected: It is possible to grab the original cause of the error, and the original localized message of the underlying URLError. Very precisely, I expect to be able to detect the
notConnectedToInternet
code ofURLError
, and confidently display the excellent French message "La connexion Internet semble interrompue." that is embedded in theNSLocalizedDescription
key of the URLError. This is very important for a quality user interface. Experience shows thatURLError
messages are very suitable for display (at least for some specific codes).What happens instead: I get a
ClientError
whoseunderlyingError
is aRuntimeError
(internal to OpenAPIRuntime) that hides the original error in itstransportFailed
case, and corrupts the localized message in itsprettyDescription
by prepending English text.In summary, please don't hide the raw errors. I opted in for
URLSession
by providingURLSessionTransport
: I expect to be able to get some genuineURLError
at some point, along with their high quality information and localized messages, without any information loss.