Skip to content

Commit 2ffc046

Browse files
committed
feat: use openrpc error types and add try_cause_as for typed error matching
Remove duplicated RpcError/RpcErrorCause from rpc_client.rs — now imported from near_openrpc_client's errors module. Simplify is_critical_* retry functions in utils.rs to use RpcError::is_retryable() and try_cause_as<RpcTransactionError>() instead of string-matching cause names. Add SendRequestError::try_cause_as<T>() convenience method so downstream users can pattern-match on typed RPC error enums (RpcQueryError, RpcTransactionError, etc.) without unwrapping. Re-export near_openrpc_client::errors as near_api::rpc_errors for easy access to the typed error enums.
1 parent 59fb706 commit 2ffc046

File tree

6 files changed

+85
-134
lines changed

6 files changed

+85
-134
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ near-gas = { version = "0.3", features = ["serde", "borsh"] }
5555
near-token = { version = "0.3", features = ["serde", "borsh"] }
5656
near-abi = "0.4.2"
5757
near-ledger = "0.9.1"
58-
near-openrpc-client = { git = "https://github.com/near/near-openrpc-client-rs.git", default-features = false }
58+
near-openrpc-client = { git = "https://github.com/near/near-openrpc-client-rs.git", branch = "feat/errors-module", default-features = false }
5959

6060
# Dev-dependencies
6161
near-primitives = { version = "0.34" }

api/src/common/query/query_rpc.rs

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use async_trait::async_trait;
22

3+
use near_openrpc_client::{RpcError, RpcErrorCause};
4+
35
use crate::{
46
NetworkConfig,
57
advanced::{RpcType, query_request::QueryRequest},
68
common::utils::{is_critical_query_error, to_retry_error},
79
config::RetryResponse,
810
errors::SendRequestError,
9-
rpc_client::{RpcCallError, RpcClient, RpcError, RpcErrorCause},
11+
rpc_client::{RpcCallError, RpcClient},
1012
};
1113
use near_api_types::Reference;
1214

@@ -15,6 +17,25 @@ pub struct SimpleQueryRpc {
1517
pub request: QueryRequest,
1618
}
1719

20+
/// Synthesize a `SendRequestError` for query-embedded execution errors.
21+
///
22+
/// NEAR's query RPC returns HTTP 200 with an `"error"` string field in the
23+
/// result body for WASM execution failures. We convert this into a proper
24+
/// `RpcError` with a `CONTRACT_EXECUTION_ERROR` cause so the retry logic
25+
/// and typed error matching work uniformly.
26+
fn query_execution_error(error_msg: &str) -> SendRequestError {
27+
SendRequestError::from(RpcCallError::Rpc(RpcError {
28+
code: -32000,
29+
message: "Server error".to_string(),
30+
data: Some(serde_json::Value::String(error_msg.to_string())),
31+
name: Some("HANDLER_ERROR".to_string()),
32+
cause: Some(RpcErrorCause {
33+
name: "CONTRACT_EXECUTION_ERROR".to_string(),
34+
info: Some(serde_json::json!({ "error_message": error_msg })),
35+
}),
36+
}))
37+
}
38+
1839
#[async_trait]
1940
impl RpcType for SimpleQueryRpc {
2041
type RpcReference = Reference;
@@ -28,41 +49,15 @@ impl RpcType for SimpleQueryRpc {
2849
let request = self.request.clone().to_rpc_query_request(reference.clone());
2950
match client.call::<_, serde_json::Value>("query", request).await {
3051
Ok(value) => {
31-
// NEAR's query method returns a successful JSON-RPC response even for
32-
// WASM execution errors. The error is embedded as an "error" string field
33-
// in the result body (e.g., {"error": "wasm execution failed...", "logs": []}).
34-
// We need to detect this and convert it to a proper RPC error.
3552
if let Some(error_msg) = value.get("error").and_then(|e| e.as_str()) {
36-
let cause_name = if error_msg.contains("CodeDoesNotExist") {
37-
"CONTRACT_EXECUTION_ERROR"
38-
} else if error_msg.contains("MethodNotFound")
39-
|| error_msg.contains("MethodResolveError")
40-
{
41-
"CONTRACT_EXECUTION_ERROR"
42-
} else {
43-
"CONTRACT_EXECUTION_ERROR"
44-
};
45-
46-
let err = SendRequestError::from(RpcCallError::Rpc(RpcError {
47-
code: -32000,
48-
message: "Server error".to_string(),
49-
data: Some(serde_json::Value::String(error_msg.to_string())),
50-
name: Some("HANDLER_ERROR".to_string()),
51-
cause: Some(RpcErrorCause {
52-
name: cause_name.to_string(),
53-
info: Some(serde_json::json!({
54-
"error_message": error_msg,
55-
})),
56-
}),
57-
}));
58-
return to_retry_error(err, is_critical_query_error);
53+
return to_retry_error(
54+
query_execution_error(error_msg),
55+
is_critical_query_error,
56+
);
5957
}
6058
RetryResponse::Ok(value)
6159
}
62-
Err(err) => {
63-
let err = SendRequestError::from(err);
64-
to_retry_error(err, is_critical_query_error)
65-
}
60+
Err(err) => to_retry_error(SendRequestError::from(err), is_critical_query_error),
6661
}
6762
}
6863
}

api/src/common/utils.rs

Lines changed: 16 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
// New errors can be added to the codebase, so we want to handle them gracefully
2-
#![allow(unreachable_patterns)]
3-
41
use base64::{Engine, prelude::BASE64_STANDARD};
52
use near_api_types::NearToken;
3+
use near_openrpc_client::{RpcError, RpcTransactionError};
64

75
use crate::{
86
config::RetryResponse,
97
errors::SendRequestError,
10-
rpc_client::{RpcCallError, RpcError},
8+
rpc_client::RpcCallError,
119
};
1210

1311
pub fn to_base64(input: &[u8]) -> String {
@@ -34,57 +32,31 @@ pub fn to_retry_error<T>(
3432
}
3533
}
3634

35+
/// Blocks errors: all known causes are retryable (UNKNOWN_BLOCK, NOT_SYNCED_YET, INTERNAL_ERROR).
3736
pub fn is_critical_blocks_error(err: &SendRequestError) -> bool {
38-
is_critical_json_rpc_error(err, |rpc_err| {
39-
match rpc_err.cause_name() {
40-
Some("UNKNOWN_BLOCK") | Some("NOT_SYNCED_YET") | Some("INTERNAL_ERROR") => false,
41-
_ => false,
42-
}
43-
})
37+
is_critical_json_rpc_error(err, |rpc_err| !rpc_err.is_retryable())
4438
}
4539

40+
/// Validator errors: all known causes are retryable (UNKNOWN_EPOCH, VALIDATOR_INFO_UNAVAILABLE, INTERNAL_ERROR).
4641
pub fn is_critical_validator_error(err: &SendRequestError) -> bool {
47-
is_critical_json_rpc_error(err, |rpc_err| {
48-
match rpc_err.cause_name() {
49-
Some("UNKNOWN_EPOCH") | Some("VALIDATOR_INFO_UNAVAILABLE") | Some("INTERNAL_ERROR") => {
50-
false
51-
}
52-
_ => false,
53-
}
54-
})
42+
is_critical_json_rpc_error(err, |rpc_err| !rpc_err.is_retryable())
5543
}
5644

45+
/// Query errors: retryable causes (NO_SYNCED_BLOCKS, UNAVAILABLE_SHARD, UNKNOWN_BLOCK, INTERNAL_ERROR)
46+
/// are not critical, but permanent errors (INVALID_ACCOUNT, UNKNOWN_ACCOUNT, etc.) are.
47+
/// NO_GLOBAL_CONTRACT_CODE is treated as retryable since it may not have propagated yet.
5748
pub fn is_critical_query_error(err: &SendRequestError) -> bool {
58-
is_critical_json_rpc_error(err, |rpc_err| {
59-
match rpc_err.cause_name() {
60-
Some("NO_SYNCED_BLOCKS") | Some("UNAVAILABLE_SHARD") | Some("UNKNOWN_BLOCK")
61-
| Some("INTERNAL_ERROR") => false,
62-
63-
Some("GARBAGE_COLLECTED_BLOCK")
64-
| Some("INVALID_ACCOUNT")
65-
| Some("UNKNOWN_ACCOUNT")
66-
| Some("NO_CONTRACT_CODE")
67-
| Some("TOO_LARGE_CONTRACT_STATE")
68-
| Some("UNKNOWN_ACCESS_KEY")
69-
| Some("CONTRACT_EXECUTION_ERROR")
70-
| Some("UNKNOWN_GAS_KEY") => true,
71-
72-
// Might be critical, but also might not yet propagated across the network, so we will retry
73-
Some("NO_GLOBAL_CONTRACT_CODE") => false,
74-
_ => false,
75-
}
76-
})
49+
is_critical_json_rpc_error(err, |rpc_err| !rpc_err.is_retryable())
7750
}
7851

52+
/// Transaction errors: TIMEOUT_ERROR and REQUEST_ROUTED are retryable.
53+
/// INVALID_TRANSACTION, DOES_NOT_TRACK_SHARD, UNKNOWN_TRANSACTION are critical.
54+
/// INTERNAL_ERROR is treated as critical for transactions (different from queries).
7955
pub fn is_critical_transaction_error(err: &SendRequestError) -> bool {
8056
is_critical_json_rpc_error(err, |rpc_err| {
81-
match rpc_err.cause_name() {
82-
Some("TIMEOUT_ERROR") | Some("REQUEST_ROUTED") => false,
83-
Some("INVALID_TRANSACTION")
84-
| Some("DOES_NOT_TRACK_SHARD")
85-
| Some("UNKNOWN_TRANSACTION")
86-
| Some("INTERNAL_ERROR") => true,
87-
_ => false,
57+
match rpc_err.try_cause_as::<RpcTransactionError>() {
58+
Some(Ok(RpcTransactionError::TimeoutError | RpcTransactionError::RequestRouted { .. })) => false,
59+
_ => true,
8860
}
8961
})
9062
}
@@ -99,7 +71,6 @@ fn is_critical_json_rpc_error(
9971
SendRequestError::TransportError(err) => match err {
10072
RpcCallError::Http(e) => {
10173
use reqwest::StatusCode;
102-
// Check HTTP status for retryable errors
10374
e.status().map_or(false, |s| {
10475
!matches!(
10576
s,

api/src/errors.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::sync::Arc;
22

3-
use crate::rpc_client::{RpcCallError, RpcError};
3+
use crate::rpc_client::RpcCallError;
4+
use near_openrpc_client::RpcError;
5+
use serde::Deserialize;
46

57
#[derive(thiserror::Error, Debug)]
68
pub enum QueryCreationError {
@@ -288,6 +290,39 @@ pub enum SendRequestError {
288290
ServerError(RpcError),
289291
}
290292

293+
impl SendRequestError {
294+
/// Returns the underlying [`RpcError`] if this is a server error.
295+
pub fn as_rpc_error(&self) -> Option<&RpcError> {
296+
match self {
297+
Self::ServerError(e) => Some(e),
298+
_ => None,
299+
}
300+
}
301+
302+
/// Attempts to deserialize the RPC error cause into a typed per-method error enum.
303+
///
304+
/// Convenience wrapper around [`RpcError::try_cause_as`]. Returns `None` if
305+
/// this isn't a server error or if the server error has no cause.
306+
///
307+
/// # Example
308+
///
309+
/// ```ignore
310+
/// use near_openrpc_client::errors::RpcQueryError;
311+
///
312+
/// match send_err.try_cause_as::<RpcQueryError>() {
313+
/// Some(Ok(RpcQueryError::UnknownAccount { requested_account_id, .. })) => {
314+
/// println!("Account {requested_account_id} not found");
315+
/// }
316+
/// _ => {}
317+
/// }
318+
/// ```
319+
pub fn try_cause_as<T: for<'de> Deserialize<'de>>(
320+
&self,
321+
) -> Option<Result<T, serde_json::Error>> {
322+
self.as_rpc_error()?.try_cause_as()
323+
}
324+
}
325+
291326
impl From<RpcCallError> for SendRequestError {
292327
fn from(err: RpcCallError) -> Self {
293328
match err {

api/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ pub mod rpc_client;
6666

6767
pub use near_api_types as types;
6868
pub mod errors;
69+
/// Re-export per-method RPC error enums for typed error matching via [`errors::SendRequestError::try_cause_as`].
70+
pub use near_openrpc_client::errors as rpc_errors;
6971
pub mod signer;
7072

7173
pub use crate::{

api/src/rpc_client.rs

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
use near_openrpc_client::RpcError;
12
use serde::{Deserialize, Serialize};
23

4+
pub use near_openrpc_client::errors;
5+
36
/// Thin JSON-RPC 2.0 client wrapping `reqwest::Client`.
47
///
58
/// This replaces the OpenAPI-generated client with a minimal transport
@@ -30,61 +33,6 @@ struct RpcResponse {
3033
error: Option<RpcError>,
3134
}
3235

33-
/// Error from a JSON-RPC call.
34-
///
35-
/// NEAR's RPC extends standard JSON-RPC errors with `name` and `cause` fields
36-
/// that carry structured, typed error information.
37-
#[derive(Debug, Clone, Deserialize)]
38-
pub struct RpcError {
39-
pub code: i64,
40-
pub message: String,
41-
/// Deprecated by nearcore. Prefer `cause` for structured error data.
42-
#[serde(default)]
43-
pub data: Option<serde_json::Value>,
44-
/// Error category: `HANDLER_ERROR`, `REQUEST_VALIDATION_ERROR`, or `INTERNAL_ERROR`.
45-
#[serde(default)]
46-
pub name: Option<String>,
47-
/// Structured error detail with per-method error variant name and info.
48-
#[serde(default)]
49-
pub cause: Option<RpcErrorCause>,
50-
}
51-
52-
/// Structured cause of an RPC error.
53-
#[derive(Debug, Clone, Deserialize)]
54-
pub struct RpcErrorCause {
55-
/// The error variant name (e.g., `UNKNOWN_BLOCK`, `INVALID_ACCOUNT`).
56-
pub name: String,
57-
/// Additional structured information about the error.
58-
#[serde(default)]
59-
pub info: Option<serde_json::Value>,
60-
}
61-
62-
impl RpcError {
63-
pub fn is_handler_error(&self) -> bool {
64-
self.name.as_deref() == Some("HANDLER_ERROR")
65-
}
66-
67-
pub fn is_request_validation_error(&self) -> bool {
68-
self.name.as_deref() == Some("REQUEST_VALIDATION_ERROR")
69-
}
70-
71-
pub fn is_internal_error(&self) -> bool {
72-
self.name.as_deref() == Some("INTERNAL_ERROR")
73-
}
74-
75-
pub fn cause_name(&self) -> Option<&str> {
76-
self.cause.as_ref().map(|c| c.name.as_str())
77-
}
78-
}
79-
80-
impl std::fmt::Display for RpcError {
81-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82-
write!(f, "RPC error {}: {}", self.code, self.message)
83-
}
84-
}
85-
86-
impl std::error::Error for RpcError {}
87-
8836
/// Errors that can occur when making a JSON-RPC call.
8937
#[derive(Debug, thiserror::Error)]
9038
pub enum RpcCallError {

0 commit comments

Comments
 (0)