-
Notifications
You must be signed in to change notification settings - Fork 5.3k
add NetworkException #40344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add NetworkException #40344
Conversation
|
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
|
I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label. |
|
Tagging subscribers to this area: @dotnet/ncl |
| protected NetworkException(NetworkError error, Exception? innerException = null, string? message = null) { } | ||
| protected NetworkException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) { } | ||
| public NetworkError NetworkError { get { throw null; } } | ||
| public static NetworkException Create(NetworkError error, Exception? innerException = null, string? message = null) { throw null; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't in the API review. Why does this use protected ctors and a factory method? That's very uncommon with exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is that we could have derived exceptions like AddressInUseException later on, and that Create would be updated to throw those when we later add them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we add such types? What additional information would the exception carry that would require a custom type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's pretty unlikely we would ever add such types. The discussion in the meeting went around a bit and this may be an artifact of some intermediate stage of dicussion.
I'd be happy to just remove the factory method on the assumption that we'll never add those types. As you say, there doesn't really seem to be any reason to add them, since you already have all the info you need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guided by @bartonjs guidance that exception filters should not be the primary method of distinguishing exceptions that you would take different actions on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll give an argument for having distinct exception types:
We are no longer giving OS error codes with this API. We have distilled/merged similar error codes together, to the point where it really doesn't seem like someone would want to take action on multiple of them in a single catch block -- the actions would be so different that multiple catch blocks are warranted.
If that is true, then having multiple exception types makes more sense and would bring us a little closer to what I view as idiomatic C#, while also providing an error code leaves an escape hatch for anyone who would be in the rare situation of needing to combine multiple error codes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're saying the code that ASP.NET has (https://source.dot.net/#Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets/Internal/SocketConnection.cs,144) that currently uses an exception filter would stop doing so? I don't buy it, especially when factoring in that it's not only looking at multiple exception types, but multiple error codes conditioned by platform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub we should consider using that logic here so that they don't have to. That should be a goal when reducing the SocketError into NetworkError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, it's about making sure that people can write their handlers correctly.
- If all errors are ignored, there's no catch, everything works.
- If all errors are retried, there's no filter, and the one exception type (which would be the base class for the specially singled-out ones) is sufficient.
- If a "common" (to whatever extent you want to call something common) pattern is to only care about (e.g.) ConnectionReset, then a distinct type has a better chance of producing code to match that desire than a filter.
- If you leave off the filter, it still catches when you wanted, but also when you didn't.
- If ConnectionReset ever gets more granular, you need to update your filter, but have no good way of knowing it, which makes an extensible-looking model be actually brittle.
In the discussion, it was suggested that ConnectionReset, ConnectionAborted, and possibly AddressInUse were the most likely things to be handled in a manner other than "any connection error". Leading to the hybrid model:
- NetworkException : IOException
- static NetworkException Create(NetworkError)
- enum NetworkError
- ConnectionResetException : NetworkException
- ConnectionAbortedException : NetworkException
- AddressInUseException : NetworkException
InvalidAddress is mostly a "something weird made it this far" (IIUC), so it seems (to me, and as suggested in the meeting) to not be something that would ever warrant a specialty type... because no one would really be handling it as a special case. And while the other errors do have their particular uses (largely in painting UI in special ways for error handling) they might be handled rarely enough that they also aren't worth specialty types.
SocketError, for example, has over 50 members; in no world would I want us adding over 50 exception type
While I'm not a sockets guru, I don't expect that there's really any recovery action for NotSocket; so it wouldn't warrant a specialty type (no special handler, no special type). The fact that NotSocket happened is definitely useful for logging, so reporting the information is useful. And maybe there's a super-power-scenario where someone can do something unique and clever, so also being programmatic isn't harmful.
Based on the suggested mappings for NetworkError from SocketError, many of the SocketError values are "similar, but different in nuance", and those would probably benefit/have-benefitted from a tiny bit of hierarchy, like UnsupportedProtocolException : SocketException, to represent ProtocolType/ProtocolOption/ProtocolNotSupported (I'm just guessing based on names).
I don't think the position follows from those CONSIDER/DO NOTs
The conclusion that I draw from them is "everything that should be handled the same way in common usage of the API should be the same exception type", and "everything that should be handled in distinct ways in common usage of the API should be different exception types"; again, using robustness and the ability to evolve as the justification. While Krzysztof's comment sort of suggests never having the programmatic properties, I don't quite get that from the guidelines. I think that the hybrid model is a nice balance for "make easy things easy and hard things possible", or a variant "make common needs easy to fulfill, and uncommon needs possible to fulfill".
The impression I got from the meeting is ConnectionAborted and ConnectionReset are "commonly" handled, and are handled differently from each other, so different exception types are warranted. The fact that the referenced ASP.Net code defined exceptions of exactly those names feels like reinforcement of that belief.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should consider using that logic here so that they don't have to. That should be a goal when reducing the SocketError into NetworkError.
ASP.NET can choose to make such policy decisions as to how it reacts to various conditions. I don't think the same holds for the library. The comment there indicates it's only sometimes returning a different code than expected; we'd be applying a broken policy if we then always did such a translation for other consumers.
idiomatic C#,
And the next catch block that handles both an abort and a dispose in the same catch block? That's not "idiomatic C#"? It sure seems to me like a completely valid, prototypical use. If what you mean by idiomatic is "wouldn't have been written before C# 7", then sure, and we may as well stop investing in the language if we say anything beyond the initial releases isn't recommended.
I honestly don't understand the argument here for derived types. If two NetworkExceptions differ only in error code, and carry no different data otherwise, I see no good reason to introduce yet another exception type. It provides zero additional benefit from my perspective, only complication.
And with a design like the one approved, nothing prevents two different derived types from using the same error code. At that point, I can't handle it just by derived type and have to use the base type with error code to be correct.
src/libraries/System.Net.Primitives/src/System/Net/NetworkException.cs
Outdated
Show resolved
Hide resolved
| } | ||
| public class NetworkException : System.IO.IOException | ||
| { | ||
| protected NetworkException(NetworkError error, Exception? innerException = null, string? message = null) { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This order is strange. message (almost?) always comes before innerException.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're ordered from most used to least used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That may be, but where else in the dozens or hundreds of exception types in .NET does this order occur? It's very inconsistent. @bartonjs, you're ok with this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order from the ctor ended up just being what we designed the factory method as; which was based on the notion that it would be throw NetworkException.Create(NetworkError.ConnectionAborted);, which would allow the easy creation of a derived type later, to avoid using exception filters as the primary means of distinguishing errors that seem catchable.
I don't think we really spent a lot of time and thinking on this line, TBH; and @geoffkizer was pushing for a fast "can we just approve it and move on", which we apparently did 😄.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@geoffkizer was pushing for a fast "can we just approve it and move on"
I plead guilty to this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By far the most common usage here is, give an error code and an optional inner exception. It's pretty weird to want to customize the error message. But I don't think we should rule it out entirely -- especially for the NetworkError.Unknown case, it's useful for the provider to be able to provide more info in the exception message.
Perhaps we could finesse this by having two overloads:
- NetworkException(NetworkError error, Exception? innerException = null)
- NetworkException(string message, NetworkError error, Exception? innerException = null)
|
I added OperationAborted as well, and fixed up the other issues above. We need to decide on: |
|
I also tried to improve the exception messages. Please take a look. |
|
Pushed a change so that SocketsHttpConnectionFactory will now throw NetworkExceptions. |
I assume use in NetworkStream will come subsequently? |
| [Fact] | ||
| public static void Create_InnerExceptionAndMessage_Success() | ||
| { | ||
| const string message = "Hello"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: message => Message
...aries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Connections/SocketConnection.cs
Outdated
Show resolved
Hide resolved
| NetworkError error = socketException.SocketErrorCode switch | ||
| { | ||
| SocketError.AddressAlreadyInUse => NetworkError.AddressInUse, | ||
| SocketError.AddressFamilyNotSupported => NetworkError.InvalidAddress, | ||
| SocketError.AddressNotAvailable => NetworkError.InvalidAddress, | ||
| SocketError.HostNotFound => NetworkError.HostNotFound, | ||
| SocketError.ConnectionRefused => NetworkError.ConnectionRefused, | ||
| SocketError.OperationAborted => NetworkError.OperationAborted, | ||
| SocketError.ConnectionAborted => NetworkError.ConnectionAborted, | ||
| SocketError.ConnectionReset => NetworkError.ConnectionReset, | ||
| _ => NetworkError.Unknown | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably doesn't matter, but code size can be reduced by using a switch statement rather than a switch expression, by having the NetworkError.InvalidAddress cases fall through to a single branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm kind of surprised that switch expressions don't support this sort of thing...
…andler/Connections/SocketConnection.cs add ExceptionDispatchInfo handling Co-authored-by: Cory Nelson <[email protected]>
Yes. |
|
Since this implements the API as approved, and since there doesn't seem to be any consensus to change it, I'm planning to merge as is when CI completes, in the interest of getting this in sooner rather than later. @stephentoub I agree with your points, and I'd be happy to raise these in a subsequent API review if you'd like me to. |
address -> EndPoint in error messages Co-authored-by: Cory Nelson <[email protected]>
address -> EndPoint in error messages Co-authored-by: Cory Nelson <[email protected]>
src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs
Outdated
Show resolved
Hide resolved
|
Pushed a change to make NetworkStream always throw NetworkException instead of IOException. Note, there's some questionable exception handling in NetworkStream currently -- specifically, lots of cases where it throws NetworkException (previously IOException), either with a wrapped inner exception or not, where it could arguably be throwing a different exception type. Examples include: (1) Constructor throws NetworkException if the underlying socket isn't appropriate (e.g. UDP). This could arguably be ArgumentException or InvalidOperationException. If there's consensus to change these exception cases, I'll happily make the change. |
|
Also pushed a change to remove SocketConnectionNetworkStream per discussion with @scalablecory. |
These would be breaking changes, though maybe some of them would be acceptable. I wouldn't do anything in this PR though. |
Agreed, though most of these cases are downright weird, so I suspect changing them is reasonable. Not super important though.
The only reason to consider doing something in this PR is that all of these non-SocketException cases result in a NetworkException with NetworkError=Unknown. That's a bit confusing. I think we really don't want to add an enum value in NetworkError for something like "you tried to create a NetworkStream on a UDP socket". But that means either live with NetworkError=Unknown, which is probably fine, or change this to some non-NetworkException thing. |
|
Or maybe change NetworkError.Unknown to NetworkError.Other. These cases are super weird and I don't think it's worth spending much time worrying about them. |
| Assert.Throws<NetworkException>(() => server.Read(new byte[1], 0, 1)); | ||
| Assert.Throws<NetworkException>(() => server.Write(new byte[1], 0, 1)); | ||
|
|
||
| Assert.Throws<IOException>(() => server.BeginRead(new byte[1], 0, 1, null, null)); | ||
| Assert.Throws<IOException>(() => server.BeginWrite(new byte[1], 0, 1, null, null)); | ||
| Assert.Throws<NetworkException>(() => server.BeginRead(new byte[1], 0, 1, null, null)); | ||
| Assert.Throws<NetworkException>(() => server.BeginWrite(new byte[1], 0, 1, null, null)); | ||
|
|
||
| Assert.Throws<IOException>(() => { server.ReadAsync(new byte[1], 0, 1); }); | ||
| Assert.Throws<IOException>(() => { server.WriteAsync(new byte[1], 0, 1); }); | ||
| Assert.Throws<NetworkException>(() => { server.ReadAsync(new byte[1], 0, 1); }); | ||
| Assert.Throws<NetworkException>(() => { server.WriteAsync(new byte[1], 0, 1); }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's a deficiency of the original tests, but shouldn't we add assertions for exception.NetworkError with this refactor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could, but these are just wrapping ObjectDisposedException so the NetworkError would be Unknown, which isn't all that useful to validate.
|
I pushed a change with the latest updates from API review. This is ready to merge, please take a look. |
src/libraries/System.Net.Primitives/src/System/Net/NetworkException.cs
Outdated
Show resolved
Hide resolved
…ption.cs Co-authored-by: Cory Nelson <[email protected]>
scalablecory
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
|
Isn't the replacement of all exceptions thrown by I think we should at least add the breaking label here as a minimum. |
We are only throwing a more-derived exception, nothing here should be breaking. |
* add NetworkException
| { | ||
| serverSocket.Dispose(); | ||
|
|
||
| Assert.Throws<IOException>(() => server.Read(new byte[1], 0, 1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just marking this diff sorry.
Contributes to #39941
Feedback on error messages would be appreciated... I mostly stole these from the SocketException messages, but those aren't all that clear to begin with.
@scalablecory @stephentoub @halter73 @davidfowl @dotnet/ncl