Skip to content

feat!: add IntoErrorPayload trait for structured JSON-RPC errors#42

Merged
prestwich merged 10 commits intomainfrom
prestwich/into-error-payload
Mar 26, 2026
Merged

feat!: add IntoErrorPayload trait for structured JSON-RPC errors#42
prestwich merged 10 commits intomainfrom
prestwich/into-error-payload

Conversation

@prestwich
Copy link
Member

@prestwich prestwich commented Mar 24, 2026

Summary

  • Adds IntoErrorPayload trait that lets handler error types control JSON-RPC error code, message, and data fields when returning Result<T, E>
  • Adds InternalError<T> wrapper for placing a value in the error data field with code -32603
  • Adds convenience impls for ErrorPayload, String, &'static str, ()
  • Adds internal IntoResponsePayload trait to unify the macro conversion path
  • Removes From<Result<T, E>> for ResponsePayload, From<T> for ErrorPayload<T> where T: Error + RpcSend, and convert_internal_error_msg
  • Adds magic8ball example showcasing custom error types with per-variant error codes and structured data
  • Bumps version to 0.7.0

Breaking changes

  • Result<T, E> handlers now require E: IntoErrorPayload instead of E: RpcSend
  • String/&str errors now populate the message field (previously went in data)
  • Removed From<Result<T, E>> for ResponsePayload, From<T> for ErrorPayload<T> where T: Error + RpcSend, and ResponsePayload::convert_internal_error_msg
  • Migration: wrap error types in InternalError<E> for the old behavior, or implement IntoErrorPayload for custom error codes

Test plan

  • Unit tests for all IntoErrorPayload impls (ErrorPayload identity, InternalError, String, &str, (), custom error type)
  • Unit tests for IntoResponsePayload (Result and ResponsePayload paths)
  • Existing handler inference tests pass
  • All doc tests compile and pass
  • Integration tests (ws, ipc, axum) pass
  • Clippy clean with --all-features and --no-default-features
  • magic8ball example compiles and demonstrates all error paths

Closes ENG-2085

🤖 Generated with Claude Code

prestwich and others added 9 commits March 24, 2026 13:04
Adds the public IntoErrorPayload trait with error_code, error_message,
error_data, and into_error_payload methods. Adds convenience impls for
ErrorPayload, InternalError<T>, String, &'static str, and ().

ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unifies Result and ResponsePayload conversion paths for the handler
macro. Result<T, E: IntoErrorPayload> delegates to into_error_payload().
ResponsePayload<T, E> passes through unchanged.

ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prepares for IntoErrorPayload by routing through the new internal
IntoResponsePayload trait instead of From<Result<T, E>>.

Also updates OutputResult handler bounds from ErrData: RpcSend to
ErrData: IntoErrorPayload, as these changes are co-dependent.

ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BREAKING: Removes From<Result<T, E>> for ResponsePayload,
From<T> for ErrorPayload<T> where T: Error + RpcSend, and
ResponsePayload::convert_internal_error_msg. These are replaced by the
IntoErrorPayload and IntoResponsePayload traits.

ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Breaking changes in this release:
- Handler Result<T, E> error types must implement IntoErrorPayload
- String/&str errors now populate the message field (not data)
- Removed From<Result> for ResponsePayload, From<T> for ErrorPayload,
  and convert_internal_error_msg

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Demonstrates custom error types with per-variant JSON-RPC error codes,
messages, and structured error data via the IntoErrorPayload trait.

ENG-2085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the blanket `From<T>` impl on `InternalError<T>` so that
wrapping a value requires explicit `InternalError(val)` construction.
This prevents silent semantic shifts when switching between `String`
(→ message field) and `InternalError<String>` (→ data field) as
handler error types.

Also gitignore /docs to prevent plan files from being committed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Contributor

@Fraser999 Fraser999 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All just non-blocking nits.

Comment on lines +103 to +111
let data = match &self {
Self::CrystalBallCloudy => serde_json::value::to_raw_value(&CloudyDetail {
visibility: "opaque",
suggestion: "try polishing the ball",
})
.ok(),
Self::StarsMisaligned { sign } => {
serde_json::value::to_raw_value(&MisalignmentDetail {
sign: sign.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let data = match &self {
Self::CrystalBallCloudy => serde_json::value::to_raw_value(&CloudyDetail {
visibility: "opaque",
suggestion: "try polishing the ball",
})
.ok(),
Self::StarsMisaligned { sign } => {
serde_json::value::to_raw_value(&MisalignmentDetail {
sign: sign.clone(),
let data = match self {
Self::CrystalBallCloudy => serde_json::value::to_raw_value(&CloudyDetail {
visibility: "opaque",
suggestion: "try polishing the ball",
})
.ok(),
Self::StarsMisaligned { sign } => {
serde_json::value::to_raw_value(&MisalignmentDetail {
sign,

Comment on lines +84 to +87
Self::CrystalBallCloudy => -32001,
Self::StarsMisaligned { .. } => -32002,
Self::MercuryRetrograde => -32003,
Self::UnreadableAura(_) => -32004,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My interpretation of the spec makes me think we shouldn't be using error codes in the range [-32099, -32000] for application errors like this, but only for errors specific to our JSON RPC implementation - i.e. for ajj to use itself.

Fut: Future<Output = Result<Payload, ErrData>> + Send + 'static,
Payload: RpcSend,
ErrData: RpcSend,
ErrData: IntoErrorPayload,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have IntoErrorPayload with an associated type ErrData, it might be helpful to rename all these ErrData type params in this file to something like E or Error?

Comment on lines +44 to +45
/// Self::NotFound(_) => 404,
/// Self::RateLimited { .. } => 429,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these look like http error codes, which could give readers the wrong impression that an http error would be returned in these cases.

-32603
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could maybe add an impl IntoErrorPayload for core::convert::Infallible?

Comment on lines +14 to +15
/// Only [`error_code`] is required. The remaining methods have sensible
/// defaults:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Only [`error_code`] is required. The remaining methods have sensible
/// defaults:
/// Only [`error_code`] and the associated type `ErrData` must be specified.
/// The remaining methods have sensible defaults:

@prestwich prestwich merged commit 4556396 into main Mar 26, 2026
6 checks passed
prestwich added a commit that referenced this pull request Mar 26, 2026
- Use application-level error codes instead of reserved JSON-RPC range
- Fix doc example codes that looked like HTTP status codes
- Add IntoErrorPayload impl for core::convert::Infallible
- Clarify that ErrData associated type is required in trait docs
- Refactor magic8ball match to take ownership, removing unnecessary clone

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants