Repository navigation
feat!: improve error handling - #130
Merged
Merged
Conversation
Frando
marked this pull request as draft
October 2, 2026 10:29
Frando
force-pushed
the
Frando/error-cases
branch
4 times, most recently
from
October 2, 2026 11:56
d43c515 to
acf91f4
Compare
Frando
force-pushed
the
Frando/feat-handler
branch
from
October 2, 2026 12:26
4aeb9e5 to
8b65463
Compare
Frando
force-pushed
the
Frando/error-cases
branch
from
October 2, 2026 12:27
659275f to
59fb956
Compare
Frando
force-pushed
the
Frando/feat-handler
branch
from
October 5, 2026 12:04
8b65463 to
08fc52e
Compare
Frando
force-pushed
the
Frando/error-cases
branch
from
October 5, 2026 12:07
59fb956 to
785399e
Compare
Frando
force-pushed
the
Frando/feat-handler
branch
from
October 5, 2026 12:20
08fc52e to
d570769
Compare
Frando
force-pushed
the
Frando/error-cases
branch
2 times, most recently
from
October 5, 2026 13:38
4327b28 to
85bc9bb
Compare
Frando
marked this pull request as ready for review
October 5, 2026 13:53
Frando
force-pushed
the
Frando/feat-handler
branch
from
October 5, 2026 13:56
d570769 to
da2f4a6
Compare
Frando
force-pushed
the
Frando/error-cases
branch
2 times, most recently
from
October 6, 2026 08:51
d372449 to
de45a66
Compare
…ustive A request that the server cannot read currently ends the whole connection without a reason. If the request does not decode, `handle_connection` returns and drops the connection, so all requests on it fail and the clients see a close with code 0. If it is too large, the server closes the connection with code 1, and the client of the bad request sees "failed to read size". The error enums also cannot get new cases after 1.0 without a breaking change. This commit makes the changes that must happen before 1.0. Error classification methods and a way to skip bad requests can follow later without a breaking change. * Adds `#[non_exhaustive]` to `Error`, `RequestError`, `WriteError`, `SendError`, and both `RecvError`s. * Stops and resets the streams of a bad request with the new code 3 (`ERROR_CODE_DECODE_FAILED`), or with code 1 if the request is too large. `Handler::handle_connection` then closes the connection with the same code. * Gives `read_request` and `read_request_raw` the error type `ReadRequestError` with the cases `MaxMessageSizeExceeded`, `InvalidRequest`, and `Connection`. For the first two cases the connection is still open, so a server with its own loop can read on. They still return `Ok(None)` for a clean close, now also for a close by this side. `ReadRequestError` converts into `io::Error` and iroh's `AcceptError`. * Renames code 2 to `ERROR_CODE_ENCODE_FAILED`, to match `ERROR_CODE_DECODE_FAILED`. * Reserves the codes 0 to 255 for irpc. `RequestError::Unreachable` stays: `stack_error` cannot derive an empty enum, and the variant exists only without the `rpc` feature. * changed: `Error`, `RequestError`, `WriteError`, `SendError`, `mpsc::RecvError`, and `oneshot::RecvError` are `#[non_exhaustive]`. Add a wildcard arm to matches. * changed: `read_request` and `read_request_raw` return `Result<Option<_>, ReadRequestError>`, not `io::Result<Option<_>>`. * changed: a server closes the connection for a bad request with code 3 (does not decode) or code 1 (too large), after it resets the streams of that request. Before, a request that does not decode dropped the connection with code 0. * changed: `rpc::ERROR_CODE_INVALID_POSTCARD` is renamed to `rpc::ERROR_CODE_ENCODE_FAILED`.
Frando
force-pushed
the
Frando/error-cases
branch
from
October 6, 2026 10:56
de45a66 to
87df029
Compare
With versioned protocols, an unknown request type is a normal case, so a server must be able to keep the connection. `Handler::skip_bad_requests(true)` makes the handler read the next request after a bad request, instead of closing the connection. It logs the skipped request at `debug`.
A remote `oneshot::Receiver` or `mpsc::Receiver` currently returns an `Io` error if a message does not decode, but does not tell the sender. The sender sees the stream stop only when the receiver is dropped, with code 0, and cannot tell why. This makes both receivers stop the stream with `ERROR_CODE_DECODE_FAILED`, the same code that a server uses for a request that does not decode. The next send of the sender fails. This must happen before 1.0: an `mpsc::Receiver` can currently skip a message that does not decode and read the next one. After this change, the channel ends. Making this change after 1.0 would break code that skips such messages. An option to skip them can come later. * changed: a remote `mpsc::Receiver` stops its stream with code 3 if a message does not decode. Further reads fail, so a receiver can no longer skip such a message and read the next one.
Frando
force-pushed
the
Frando/error-cases
branch
from
October 6, 2026 11:01
87df029 to
79db976
Compare
Collaborator
|
Looks good overall. One real issue: Reset streams are classified as Smaller things:
|
When an irpc client drops the future of `rpc` in the middle of a write, noq finishes the stream, so the server sees a truncated request. A client can also reset the stream. The server treated both as a request that does not decode: it reset the streams with code 3, and by default closed the connection with code 3. So a single cancelled request ended all other requests on the connection. The client abandoned such a request. It did not send bad data. The server now skips it and reads the next request. It resets its side of the stream with code 0, because a drop would finish the stream, and a client that still reads would take that for an empty response. If the connection ended in the middle of a request, the next `accept_bi` returns why. So a close with code 0 or a local close now gives `Ok(None)` there too, as it does between requests.
The reservation was a note on one constant, said "must", and covered 0 to 255, but irpc does not check it and needs only a few codes. Describe all irpc codes in one module section, and say what an application risks if it uses a code below 16. The auth example used code 1, which irpc uses for a message that is too large, so it now uses 400 and 401.
When the future of a request dropped in the middle of the write, the stream of the request ended normally. So the server could not tell a cancelled request from a broken one, and it skipped both. A `RemoteSender` now resets its stream with the new code `ERROR_CODE_ABORTED` if it drops before the request is written. The server now skips a request only if its stream was reset. A stream that ends before its request is complete is a request that does not decode, so `skip_bad_requests` decides about it. The server skips a reset with any code, not only `ERROR_CODE_ABORTED`. The update sender of a request resets the stream with code 1 or 2 if an update fails, and noq drops unread data on a reset, so the server can see that reset before the request.
`read_request_frame` returned `Ok(None)` both for a stream that the client reset and for a lost connection, and matched all read errors with a wildcard. So a lost connection logged a skipped request and reset a stream on a connection that was gone. It now returns a private `ReadFrame` enum that names each outcome, the two bad requests included, so the caller handles all of them in one match. An exhaustive match on `noq::ReadError` says why each variant lands where it does. The new test checks that a close in the middle of a request gives `Ok(None)` from `read_request`, as a close between requests does.
`ReadRequestError::InvalidRequest` covered a request whose stream ended early, a size prefix that is too long, and a request that postcard cannot decode. Only the last one is the version mismatch that `skip_bad_requests` exists for, and a caller had to downcast an `io::Error` to get at its postcard error. It is now `DecodeFailed` with the `postcard::Error` as its source. `InvalidRequest` keeps the framing errors. Both still reset the streams with code 3.
Member
Author
|
Thanks. I pushed a few more commits.
Please give it another review. |
`read_next` matched `Connection` first and then asked for the error code with an `if let` that always matched. It now matches on `error_code()` directly: no code means a connection error, and a code means a bad request.
Collaborator
rklaehn
approved these changes
Oct 9, 2026
rklaehn
left a comment
Collaborator
There was a problem hiding this comment.
My small PRs on top can be merged with this or later.
This was referenced Oct 9, 2026
A few minor tweaks to the error refactor (#130): - Proper docs formatting - Also send correct error codes when using try_send - Use an explicit function instead of a default when taking `NoqSendState`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains changes to error handling.
Currently, when the server receives a request that it cannot decode, it ends the connection unconditionally.
handle_connectionreturns, and all other requests on the connection fail too. When a request is too large, the server closes the connection with code 1, and the client only sees "failed to read size".Improve handling of bad requests
When the server cannot read a request, it now stops and resets the streams of that request. It uses code 1 if the request is too large, and the new code 3 (
ERROR_CODE_DECODE_FAILED) if the request does not decode. The client of that request gets a clear error. What happens next depends on the server:Handler::handle_connectioncloses the connection with the same code, as for any other protocol violation.Handler::skip_bad_requests(true), the handler reads the next request instead. This is for protocols that add request types over time.read_requestandread_request_rawreturn the newReadRequestError, so a server with its own loop decides. AfterMaxMessageSizeExceededandInvalidRequest, the connection is still open and the server can read on. They returnConnectionwhen the connection fails, also in the middle of a request. They still returnOk(None)when the connection closes cleanly, now also when this side closed it.ReadRequestErrorconverts intoio::Errorand iroh'sAcceptError, so existing?calls keep working.Improve handling of bad channel messages
A remote
oneshot::Receiverormpsc::Receivernow stops its stream with code 3 when a message does not decode, so the sender learns why. Formpsc, this ends the channel. Before, a receiver could skip such a message and read the next one. We must make this change before 1.0, because after 1.0 it would break code that skips such messages. An option to skip them can come later.Better error types
Error,RequestError,WriteError,SendError, and bothRecvErrors are#[non_exhaustive], so they can get new cases after 1.0.ERROR_CODE_INVALID_POSTCARDtoERROR_CODE_ENCODE_FAILED, to matchERROR_CODE_DECODE_FAILED.Breaking changes
Error,RequestError,WriteError,SendError,mpsc::RecvError, andoneshot::RecvErrorare#[non_exhaustive]. Add a wildcard arm to matches.read_requestandread_request_rawreturnResult<Option<_>, ReadRequestError>, notio::Result<Option<_>>.mpsc::Receiverstops its stream with code 3 if a message does not decode. Further reads fail, so a receiver can no longer skip such a message and read the next one.rpc::ERROR_CODE_INVALID_POSTCARDis renamed torpc::ERROR_CODE_ENCODE_FAILED.