From 8cf7d8c4337abc1b9e195536b3108a2c0ece8923 Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Tue, 15 Oct 2024 15:04:37 +0200 Subject: [PATCH 1/5] WIP error details --- runtimes/core/src/api/auth/local.rs | 2 ++ runtimes/core/src/api/call.rs | 6 ++++++ runtimes/core/src/api/endpoint.rs | 2 ++ runtimes/core/src/api/error.rs | 5 +++++ runtimes/core/src/api/gateway/router.rs | 2 ++ runtimes/core/src/api/jsonschema/parse.rs | 14 ++++++++++++++ runtimes/core/src/api/manager.rs | 1 + runtimes/core/src/api/schema/body.rs | 2 ++ runtimes/core/src/api/schema/header.rs | 10 ++++++++++ runtimes/core/src/api/schema/path.rs | 9 +++++++++ runtimes/core/src/api/schema/query.rs | 1 + runtimes/core/src/api/server.rs | 2 ++ runtimes/core/src/api/static_assets.rs | 2 ++ runtimes/core/src/api/websocket.rs | 2 ++ runtimes/core/src/pubsub/gcp/push_sub.rs | 1 + runtimes/js/encore.dev/api/error.ts | 5 +++-- runtimes/js/src/api.rs | 14 ++++++++++++++ runtimes/js/src/gateway.rs | 16 ++++++++++++++++ runtimes/js/src/pubsub.rs | 12 ++++++++++++ runtimes/js/src/raw_api.rs | 12 ++++++++++++ 20 files changed, 118 insertions(+), 2 deletions(-) diff --git a/runtimes/core/src/api/auth/local.rs b/runtimes/core/src/api/auth/local.rs index c01faf019c..4996ce7e47 100644 --- a/runtimes/core/src/api/auth/local.rs +++ b/runtimes/core/src/api/auth/local.rs @@ -119,6 +119,7 @@ impl AuthHandler for LocalAuthHandler { "auth handler did not return a userID field".to_string(), ), stack: None, + details: None, }), } } @@ -127,6 +128,7 @@ impl AuthHandler for LocalAuthHandler { message: "unauthenticated".to_string(), internal_message: Some("auth handler returned null".to_string()), stack: None, + details: None, }), Err(e) => Err(e), }; diff --git a/runtimes/core/src/api/call.rs b/runtimes/core/src/api/call.rs index ab52d738ec..fdd1d3a862 100644 --- a/runtimes/core/src/api/call.rs +++ b/runtimes/core/src/api/call.rs @@ -154,6 +154,7 @@ impl ServiceRegistry { endpoint_name.service() )), stack: None, + details: None, })?; let Some(endpoint) = self.endpoints.get(endpoint_name) else { @@ -165,6 +166,7 @@ impl ServiceRegistry { endpoint_name )), stack: None, + details: None, }); }; @@ -180,6 +182,7 @@ impl ServiceRegistry { endpoint_name )), stack: None, + details: None, })?; let mut req = self @@ -207,6 +210,7 @@ impl ServiceRegistry { message: "internal error".into(), internal_message: Some("cannot make api calls to raw endpoints".to_string()), stack: None, + details: None, }); } } @@ -239,6 +243,7 @@ impl ServiceRegistry { endpoint.name.service() )), stack: None, + details: None, })?; let caller = match source { @@ -328,6 +333,7 @@ where message: body, internal_message: None, stack: None, + details: None, }) } } diff --git a/runtimes/core/src/api/endpoint.rs b/runtimes/core/src/api/endpoint.rs index 516ce5398c..bf4c08cce6 100644 --- a/runtimes/core/src/api/endpoint.rs +++ b/runtimes/core/src/api/endpoint.rs @@ -475,6 +475,7 @@ impl EndpointHandler { message: "endpoint not found".into(), internal_message: Some("the endpoint was found, but is not exposed".into()), stack: None, + details: None, } .into_response(); } else if self.endpoint.requires_auth && !request.has_authenticated_user() { @@ -483,6 +484,7 @@ impl EndpointHandler { message: "endpoint requires auth but none provided".into(), internal_message: None, stack: None, + details: None, } .into_response(); } diff --git a/runtimes/core/src/api/error.rs b/runtimes/core/src/api/error.rs index 845ce1d063..78466e1288 100644 --- a/runtimes/core/src/api/error.rs +++ b/runtimes/core/src/api/error.rs @@ -12,6 +12,7 @@ pub struct Error { pub code: ErrCode, pub message: String, pub internal_message: Option, + pub details: Option, #[serde(skip_serializing)] pub stack: Option, @@ -27,6 +28,7 @@ impl Error { message: ErrCode::Internal.default_public_message().into(), internal_message: Some(format!("{:#?}", cause.into())), stack: None, + details: None, } } @@ -40,6 +42,7 @@ impl Error { message: public_msg.into(), internal_message: Some(format!("{:#?}", cause.into())), stack: None, + details: None, } } @@ -52,6 +55,7 @@ impl Error { message: public_msg.into(), internal_message: None, stack: None, + details: None, } } @@ -72,6 +76,7 @@ impl From for Error { message: value.body_text(), internal_message: Some(value.body_text()), stack: None, + details: None, } } } diff --git a/runtimes/core/src/api/gateway/router.rs b/runtimes/core/src/api/gateway/router.rs index 6131dd8a0b..d52399c594 100644 --- a/runtimes/core/src/api/gateway/router.rs +++ b/runtimes/core/src/api/gateway/router.rs @@ -101,6 +101,7 @@ impl Router { message: "no route for method".to_string(), internal_message: Some(format!("no route for method {:?}: {}", method, path)), stack: None, + details: None, } } else { api::Error { @@ -108,6 +109,7 @@ impl Router { message: "endpoint not found".to_string(), internal_message: Some(format!("no such endpoint exists: {}", path)), stack: None, + details: None, } }) } diff --git a/runtimes/core/src/api/jsonschema/parse.rs b/runtimes/core/src/api/jsonschema/parse.rs index e885cb4e6b..adb8fbbb50 100644 --- a/runtimes/core/src/api/jsonschema/parse.rs +++ b/runtimes/core/src/api/jsonschema/parse.rs @@ -19,6 +19,7 @@ macro_rules! header_to_str { message: "invalid header value".to_string(), internal_message: Some(format!("invalid header value: {}", err)), stack: None, + details: None, }) }; } @@ -43,6 +44,7 @@ where message: format!("missing required header: {}", header_name), internal_message: None, stack: None, + details: None, }); } }; @@ -130,6 +132,7 @@ fn parse_header_value(header: &str, reg: &Registry, schema: &Value) -> APIResult message: "invalid header value".to_string(), internal_message: Some(format!("invalid float value: {}", header)), stack: None, + details: None, }) } } @@ -139,6 +142,7 @@ fn parse_header_value(header: &str, reg: &Registry, schema: &Value) -> APIResult message: "invalid header value".to_string(), internal_message: Some(format!("expected {}, got {}", want.expecting(), header)), stack: None, + details: None, }), }, @@ -162,6 +166,7 @@ fn parse_header_value(header: &str, reg: &Registry, schema: &Value) -> APIResult message: "invalid header value".to_string(), internal_message: Some(format!("no union value matched: {}", header)), stack: None, + details: None, }) } } @@ -224,6 +229,7 @@ fn parse_json_value( message: "invalid value".to_string(), internal_message: Some(format!("expected {}, got {:#?}", lit.expecting(), got)), stack: None, + details: None, }) }; @@ -326,6 +332,7 @@ fn parse_json_value( message: "invalid value".to_string(), internal_message: Some(format!("no union type matched: {}", describe_json(&this),)), stack: None, + details: None, }) } } @@ -341,6 +348,7 @@ fn unexpected_json(reg: &Registry, schema: &Value, value: &JSON) -> APIResult(reg: &Registry, schema: &Value) -> APIResult { schema.expecting(reg), )), stack: None, + details: None, }) } @@ -392,6 +401,7 @@ fn parse_basic_json( message: format!("invalid boolean value: {}", str), internal_message: None, stack: None, + details: None, }), }, Basic::Number => serde_json::Number::from_str(str) @@ -401,6 +411,7 @@ fn parse_basic_json( message: format!("invalid number value: {}", str), internal_message: None, stack: None, + details: None, }), Basic::Null if str == "null" => Ok(JSON::Null), @@ -425,6 +436,7 @@ fn parse_basic_str(basic: &Basic, str: &str) -> APIResult { message: format!("invalid boolean value: {}", str), internal_message: None, stack: None, + details: None, }), }, @@ -435,6 +447,7 @@ fn parse_basic_str(basic: &Basic, str: &str) -> APIResult { message: format!("invalid number value: {}", str), internal_message: None, stack: None, + details: None, }), _ => Err(api::Error { @@ -442,6 +455,7 @@ fn parse_basic_str(basic: &Basic, str: &str) -> APIResult { message: "invalid value".to_string(), internal_message: Some(format!("expected {}, got {:#?}", basic.expecting(), str)), stack: None, + details: None, }), } } diff --git a/runtimes/core/src/api/manager.rs b/runtimes/core/src/api/manager.rs index 88c409af21..41c2af583a 100644 --- a/runtimes/core/src/api/manager.rs +++ b/runtimes/core/src/api/manager.rs @@ -329,6 +329,7 @@ impl Manager { message: "endpoint not found".to_string(), internal_message: Some(format!("no such endpoint exists: {}", req.uri().path())), stack: None, + details: None, } .into_response() } diff --git a/runtimes/core/src/api/schema/body.rs b/runtimes/core/src/api/schema/body.rs index 270801058e..722561df11 100644 --- a/runtimes/core/src/api/schema/body.rs +++ b/runtimes/core/src/api/schema/body.rs @@ -53,6 +53,7 @@ impl ToOutgoingRequest for Body { message: "missing body payload".to_string(), internal_message: None, stack: None, + details: None, }); }; @@ -76,6 +77,7 @@ impl Body { message: "missing body payload".to_string(), internal_message: None, stack: None, + details: None, }); }; diff --git a/runtimes/core/src/api/schema/header.rs b/runtimes/core/src/api/schema/header.rs index cef6d984bd..8badecd776 100644 --- a/runtimes/core/src/api/schema/header.rs +++ b/runtimes/core/src/api/schema/header.rs @@ -173,6 +173,7 @@ impl ToOutgoingRequest for Header { message: "missing query parameters".to_string(), internal_message: Some("missing query parameters".to_string()), stack: None, + details: None, }); }; @@ -219,6 +220,7 @@ impl ToResponse for Header { message: "missing query parameters".to_string(), internal_message: Some("missing query parameters".to_string()), stack: None, + details: None, }); }; @@ -267,6 +269,7 @@ fn to_reqwest_header_value(value: &serde_json::Value) -> APIResult { @@ -276,6 +279,7 @@ fn to_reqwest_header_value(value: &serde_json::Value) -> APIResult APIResult APIResult APIResult { message: "unable to convert string to header value".to_string(), internal_message: Some(format!("unable to convert string to header value: {}", e)), stack: None, + details: None, })?, Number(num) => { @@ -335,6 +342,7 @@ fn to_axum_header_value(value: &serde_json::Value) -> APIResult { message: "unable to convert number to header value".to_string(), internal_message: Some(format!("unable to convert number to header value: {}", e)), stack: None, + details: None, })? } @@ -349,6 +357,7 @@ fn to_axum_header_value(value: &serde_json::Value) -> APIResult { message: "nested array type unsupported as header value".into(), internal_message: None, stack: None, + details: None, }) } } @@ -362,6 +371,7 @@ fn to_axum_header_value(value: &serde_json::Value) -> APIResult { message: "map type unsupported as header value".into(), internal_message: None, stack: None, + details: None, }) } })) diff --git a/runtimes/core/src/api/schema/path.rs b/runtimes/core/src/api/schema/path.rs index c66a72dd49..daa2e60337 100644 --- a/runtimes/core/src/api/schema/path.rs +++ b/runtimes/core/src/api/schema/path.rs @@ -139,6 +139,7 @@ impl Path { &name )), stack: None, + details: None, }); }; @@ -152,6 +153,7 @@ impl Path { &name )), stack: None, + details: None, }); }; @@ -178,6 +180,7 @@ impl Path { &name )), stack: None, + details: None, }) } } @@ -205,6 +208,7 @@ impl Path { message: "unable to parse path params".into(), internal_message: Some("polling path params returned pending".into()), stack: None, + details: None, })?; match result { @@ -227,6 +231,7 @@ impl Path { message: "path parameter is not a valid number".into(), internal_message: Some(err.to_string()), stack: None, + details: None, })?; serde_json::Value::Number(val) } @@ -237,6 +242,7 @@ impl Path { message: "path parameter is not a valid boolean".into(), internal_message: Some(err.to_string()), stack: None, + details: None, } })?; serde_json::Value::Bool(val) @@ -258,18 +264,21 @@ impl Path { message: "unable to parse path params".into(), internal_message: Some(err.to_string()), stack: None, + details: None, }, PathRejection::MissingPathParams(err) => api::Error { code: api::ErrCode::InvalidArgument, message: "missing path params".into(), internal_message: Some(err.to_string()), stack: None, + details: None, }, err => api::Error { code: api::ErrCode::Internal, message: "unable to parse path params".into(), internal_message: Some(err.to_string()), stack: None, + details: None, }, }), } diff --git a/runtimes/core/src/api/schema/query.rs b/runtimes/core/src/api/schema/query.rs index 31fbfe3747..35165387d9 100644 --- a/runtimes/core/src/api/schema/query.rs +++ b/runtimes/core/src/api/schema/query.rs @@ -85,6 +85,7 @@ impl ToOutgoingRequest for Query { message: "missing query parameters".to_string(), internal_message: Some("missing query parameters".to_string()), stack: None, + details: None, }); }; diff --git a/runtimes/core/src/api/server.rs b/runtimes/core/src/api/server.rs index b638e6fdf3..ac36ed370b 100644 --- a/runtimes/core/src/api/server.rs +++ b/runtimes/core/src/api/server.rs @@ -58,6 +58,7 @@ impl Server { message: "endpoint not found".to_string(), internal_message: Some(format!("no such endpoint exists: {}", req.uri().path())), stack: None, + details: None, } .into_response() } @@ -253,6 +254,7 @@ where message: "endpoint not found".to_string(), internal_message: Some("no handler registered for endpoint".to_string()), stack: None, + details: None, } .into_response(); std::task::Poll::Ready(resp) diff --git a/runtimes/core/src/api/static_assets.rs b/runtimes/core/src/api/static_assets.rs index 19692166a4..d9300a5e4c 100644 --- a/runtimes/core/src/api/static_assets.rs +++ b/runtimes/core/src/api/static_assets.rs @@ -107,6 +107,7 @@ impl BoxedHandler for StaticAssetsHandler { internal_message: None, message: "method not allowed".to_string(), stack: None, + details: None, })), axum::http::StatusCode::INTERNAL_SERVER_ERROR => { ResponseData::Typed(Err(Error { @@ -114,6 +115,7 @@ impl BoxedHandler for StaticAssetsHandler { internal_message: None, message: "failed to serve static asset".to_string(), stack: None, + details: None, })) } code => ResponseData::Typed(Err(Error::internal(anyhow::anyhow!( diff --git a/runtimes/core/src/api/websocket.rs b/runtimes/core/src/api/websocket.rs index a2ffad47e1..837ade7a8f 100644 --- a/runtimes/core/src/api/websocket.rs +++ b/runtimes/core/src/api/websocket.rs @@ -306,6 +306,7 @@ impl SocketSchema { message: "invalid streaming body type in schema".to_string(), internal_message: None, stack: None, + details: None, }); }; @@ -317,6 +318,7 @@ impl SocketSchema { message: "missing payload".to_string(), internal_message: None, stack: None, + details: None, })?; Ok(value) diff --git a/runtimes/core/src/pubsub/gcp/push_sub.rs b/runtimes/core/src/pubsub/gcp/push_sub.rs index 293ca35245..ad4b76bbf6 100644 --- a/runtimes/core/src/pubsub/gcp/push_sub.rs +++ b/runtimes/core/src/pubsub/gcp/push_sub.rs @@ -137,6 +137,7 @@ impl Inner { message: "no handler registered for subscription".to_string(), internal_message: None, stack: None, + details: None, }); }; handler diff --git a/runtimes/js/encore.dev/api/error.ts b/runtimes/js/encore.dev/api/error.ts index 227da9271f..b1efb36d7d 100644 --- a/runtimes/js/encore.dev/api/error.ts +++ b/runtimes/js/encore.dev/api/error.ts @@ -3,6 +3,7 @@ export class APIError extends Error { * The error code. */ public readonly code: ErrCode; + public details: object | undefined; // Constructs an APIError with the Canceled error code. static canceled(msg: string, cause?: Error) { @@ -95,7 +96,7 @@ export class APIError extends Error { Object.defineProperty(this, "name", { value: "APIError", enumerable: false, - configurable: true, + configurable: true }); // fix the prototype chain @@ -304,5 +305,5 @@ export enum ErrCode { * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ - Unauthenticated = "unauthenticated", + Unauthenticated = "unauthenticated" } diff --git a/runtimes/js/src/api.rs b/runtimes/js/src/api.rs index a384026e66..312a60a1a9 100644 --- a/runtimes/js/src/api.rs +++ b/runtimes/js/src/api.rs @@ -102,9 +102,20 @@ impl PromiseHandler for APIPromiseHandler { code: api::ErrCode::Internal, message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), + details: None, stack: None, })?; + log::info!("extracting details api"); + // Get the details field. + let details: Option = obj + .get_named_property::("details") + .and_then(|val| val.coerce_to_object()) + .and_then(|val| env.from_js_value(val)) + .map(Some) + .unwrap_or(None); + log::info!("extracted details api, {:?}", details); + // Get the message field. let mut message: String = obj .get_named_property::("message") @@ -114,6 +125,7 @@ impl PromiseHandler for APIPromiseHandler { code: api::ErrCode::Internal, message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), + details: None, stack: None, })?; @@ -146,6 +158,7 @@ impl PromiseHandler for APIPromiseHandler { message, stack, internal_message, + details, }) } @@ -155,6 +168,7 @@ impl PromiseHandler for APIPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some(err.to_string()), stack: None, + details: None, }) } } diff --git a/runtimes/js/src/gateway.rs b/runtimes/js/src/gateway.rs index 2a7abd9597..9f81a060c6 100644 --- a/runtimes/js/src/gateway.rs +++ b/runtimes/js/src/gateway.rs @@ -128,8 +128,21 @@ impl PromiseHandler for AuthPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), stack: None, + details: None, })?; + log::info!("extracting details gateway"); + + // Get the details field. + let details: Option = obj + .get_named_property::("details") + .and_then(|val| val.coerce_to_object()) + .and_then(|val| env.from_js_value(val)) + .map(Some) + .unwrap_or(None); + + log::info!("extracted details gateway"); + // Get the message field. let mut message: String = obj .get_named_property::("message") @@ -140,6 +153,7 @@ impl PromiseHandler for AuthPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), stack: None, + details: None, })?; // Get the error code field. @@ -171,6 +185,7 @@ impl PromiseHandler for AuthPromiseHandler { message, stack, internal_message, + details, }) } @@ -180,6 +195,7 @@ impl PromiseHandler for AuthPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some(err.to_string()), stack: None, + details: None, }) } } diff --git a/runtimes/js/src/pubsub.rs b/runtimes/js/src/pubsub.rs index 39222fcd65..bdc92e4420 100644 --- a/runtimes/js/src/pubsub.rs +++ b/runtimes/js/src/pubsub.rs @@ -155,8 +155,17 @@ impl PromiseHandler for SubscriptionPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), stack: None, + details: None, })?; + // Get the details field. + let details: Option = obj + .get_named_property::("details") + .and_then(|val| val.coerce_to_object()) + .and_then(|val| env.from_js_value(val)) + .map(Some) + .unwrap_or(None); + // Get the message field. let message: String = obj .get_named_property::("message") @@ -167,6 +176,7 @@ impl PromiseHandler for SubscriptionPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), stack: None, + details: None, })?; // Get the error code field. @@ -192,6 +202,7 @@ impl PromiseHandler for SubscriptionPromiseHandler { message, stack, internal_message: None, + details, }) } @@ -201,6 +212,7 @@ impl PromiseHandler for SubscriptionPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some(err.to_string()), stack: None, + details: None, }) } } diff --git a/runtimes/js/src/raw_api.rs b/runtimes/js/src/raw_api.rs index fba512fa5a..2b52f6faa8 100644 --- a/runtimes/js/src/raw_api.rs +++ b/runtimes/js/src/raw_api.rs @@ -487,8 +487,17 @@ impl PromiseHandler for RawPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), stack: None, + details:None, })?; + // Get the details field. + let details: Option = obj + .get_named_property::("details") + .and_then(|val| val.coerce_to_object()) + .and_then(|val| env.from_js_value(val)) + .map(Some) + .unwrap_or(None); + // Get the message field. let mut message: String = obj .get_named_property::("message") @@ -499,6 +508,7 @@ impl PromiseHandler for RawPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some("an unknown exception was thrown".into()), stack: None, + details: None, })?; // Get the error code field. @@ -530,6 +540,7 @@ impl PromiseHandler for RawPromiseHandler { message, stack, internal_message, + details, }) } @@ -539,6 +550,7 @@ impl PromiseHandler for RawPromiseHandler { message: api::ErrCode::Internal.default_public_message().into(), internal_message: Some(err.to_string()), stack: None, + details: None, }) } } From 43a6c88aa89001fc32cd585627421b206339c80c Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Wed, 16 Oct 2024 11:54:00 +0200 Subject: [PATCH 2/5] re-use code --- runtimes/core/src/api/error.rs | 2 +- runtimes/js/encore.dev/api/error.ts | 7 ++- runtimes/js/src/api.rs | 65 +------------------------- runtimes/js/src/error.rs | 72 +++++++++++++++++++++++++++++ runtimes/js/src/gateway.rs | 69 ++------------------------- runtimes/js/src/lib.rs | 1 + runtimes/js/src/pubsub.rs | 59 ++--------------------- runtimes/js/src/raw_api.rs | 65 ++------------------------ 8 files changed, 90 insertions(+), 250 deletions(-) create mode 100644 runtimes/js/src/error.rs diff --git a/runtimes/core/src/api/error.rs b/runtimes/core/src/api/error.rs index 78466e1288..924b368993 100644 --- a/runtimes/core/src/api/error.rs +++ b/runtimes/core/src/api/error.rs @@ -12,7 +12,7 @@ pub struct Error { pub code: ErrCode, pub message: String, pub internal_message: Option, - pub details: Option, + pub details: Option>, #[serde(skip_serializing)] pub stack: Option, diff --git a/runtimes/js/encore.dev/api/error.ts b/runtimes/js/encore.dev/api/error.ts index b1efb36d7d..3aa7eadfd0 100644 --- a/runtimes/js/encore.dev/api/error.ts +++ b/runtimes/js/encore.dev/api/error.ts @@ -3,7 +3,7 @@ export class APIError extends Error { * The error code. */ public readonly code: ErrCode; - public details: object | undefined; + public readonly details?: ErrDetails; // Constructs an APIError with the Canceled error code. static canceled(msg: string, cause?: Error) { @@ -86,10 +86,11 @@ export class APIError extends Error { } // Constructs an APIError with the given error code, message, and (optionally) cause. - constructor(code: ErrCode, msg: string, cause?: Error) { + constructor(code: ErrCode, msg: string, cause?: Error, details?: ErrDetails) { // extending errors causes issues after you construct them, unless you apply the following fixes super(msg, { cause }); this.code = code; + this.details = details; // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors @@ -113,6 +114,8 @@ export class APIError extends Error { } } +export interface ErrDetails {} + export enum ErrCode { /** * OK indicates the operation was successful. diff --git a/runtimes/js/src/api.rs b/runtimes/js/src/api.rs index 312a60a1a9..305937f43f 100644 --- a/runtimes/js/src/api.rs +++ b/runtimes/js/src/api.rs @@ -1,4 +1,4 @@ -use crate::log::parse_js_stack; +use crate::error::coerce_to_api_error; use crate::napi_util::{await_promise, PromiseHandler}; use crate::request_meta::RequestMeta; use crate::threadsafe_function::{ @@ -98,68 +98,7 @@ impl PromiseHandler for APIPromiseHandler { } fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output { - let obj = val.coerce_to_object().map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - details: None, - stack: None, - })?; - - log::info!("extracting details api"); - // Get the details field. - let details: Option = obj - .get_named_property::("details") - .and_then(|val| val.coerce_to_object()) - .and_then(|val| env.from_js_value(val)) - .map(Some) - .unwrap_or(None); - log::info!("extracted details api, {:?}", details); - - // Get the message field. - let mut message: String = obj - .get_named_property::("message") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value(val)) - .map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - details: None, - stack: None, - })?; - - // Get the error code field. - let code: api::ErrCode = obj - .get_named_property::("code") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value::(val)) - .map(|val| { - val.parse::() - .unwrap_or(api::ErrCode::Internal) - }) - .unwrap_or(api::ErrCode::Internal); - - // Get the JS stack - let stack = obj - .get_named_property::("stack") - .and_then(|val| parse_js_stack(&env, val)) - .map(Some) - .unwrap_or(None); - - let mut internal_message = None; - if code == api::ErrCode::Internal { - internal_message = Some(message); - message = api::ErrCode::Internal.default_public_message().into(); - } - - Err(api::Error { - code, - message, - stack, - internal_message, - details, - }) + Err(coerce_to_api_error(env, val)?) } fn error(&self, _: Env, err: napi::Error) -> Self::Output { diff --git a/runtimes/js/src/error.rs b/runtimes/js/src/error.rs new file mode 100644 index 0000000000..3ee6e34b0f --- /dev/null +++ b/runtimes/js/src/error.rs @@ -0,0 +1,72 @@ +use crate::log::parse_js_stack; +use encore_runtime_core::api; +use napi::{Env, JsUnknown, Status}; + +pub fn coerce_to_api_error(env: Env, val: napi::JsUnknown) -> Result { + let obj = val.coerce_to_object().map_err(|_| api::Error { + code: api::ErrCode::Internal, + message: api::ErrCode::Internal.default_public_message().into(), + internal_message: Some("an unknown exception was thrown".into()), + details: None, + stack: None, + })?; + + // Get the details field. + let details: Option> = obj + .get_named_property::("details") + .and_then(|val| match val.get_type()? { + napi::ValueType::Object => val.coerce_to_object(), + _ => Err(napi::Error::new( + Status::InvalidArg, + "details can only be object", + )), + }) + .and_then(|val| env.from_js_value(val)) + .map(Some) + .unwrap_or(None); + + // Get the message field. + let mut message: String = obj + .get_named_property::("message") + .and_then(|val| val.coerce_to_string()) + .and_then(|val| env.from_js_value(val)) + .map_err(|_| api::Error { + code: api::ErrCode::Internal, + message: api::ErrCode::Internal.default_public_message().into(), + internal_message: Some("an unknown exception was thrown".into()), + details: None, + stack: None, + })?; + + // Get the error code field. + let code: api::ErrCode = obj + .get_named_property::("code") + .and_then(|val| val.coerce_to_string()) + .and_then(|val| env.from_js_value::(val)) + .map(|val| { + val.parse::() + .unwrap_or(api::ErrCode::Internal) + }) + .unwrap_or(api::ErrCode::Internal); + + // Get the JS stack + let stack = obj + .get_named_property::("stack") + .and_then(|val| parse_js_stack(&env, val)) + .map(Some) + .unwrap_or(None); + + let mut internal_message = None; + if code == api::ErrCode::Internal { + internal_message = Some(message); + message = api::ErrCode::Internal.default_public_message().into(); + } + + Ok(api::Error { + code, + message, + stack, + internal_message, + details, + }) +} diff --git a/runtimes/js/src/gateway.rs b/runtimes/js/src/gateway.rs index 9f81a060c6..7c570d1ade 100644 --- a/runtimes/js/src/gateway.rs +++ b/runtimes/js/src/gateway.rs @@ -1,12 +1,12 @@ use crate::api::Request; -use crate::log::parse_js_stack; +use crate::error::coerce_to_api_error; use crate::napi_util::{await_promise, PromiseHandler}; use crate::threadsafe_function::{ ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, }; use encore_runtime_core::api; use encore_runtime_core::api::schema; -use napi::{Env, JsFunction, JsUnknown, NapiRaw}; +use napi::{Env, JsFunction, NapiRaw}; use napi_derive::napi; use std::future::Future; use std::pin::Pin; @@ -123,70 +123,7 @@ impl PromiseHandler for AuthPromiseHandler { } fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output { - let obj = val.coerce_to_object().map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - stack: None, - details: None, - })?; - - log::info!("extracting details gateway"); - - // Get the details field. - let details: Option = obj - .get_named_property::("details") - .and_then(|val| val.coerce_to_object()) - .and_then(|val| env.from_js_value(val)) - .map(Some) - .unwrap_or(None); - - log::info!("extracted details gateway"); - - // Get the message field. - let mut message: String = obj - .get_named_property::("message") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value(val)) - .map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - stack: None, - details: None, - })?; - - // Get the error code field. - let code: api::ErrCode = obj - .get_named_property::("code") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value::(val)) - .map(|val| { - val.parse::() - .unwrap_or(api::ErrCode::Internal) - }) - .unwrap_or(api::ErrCode::Internal); - - // Get the JS stack - let stack = obj - .get_named_property::("stack") - .and_then(|val| parse_js_stack(&env, val)) - .map(Some) - .unwrap_or(None); - - let mut internal_message = None; - if code == api::ErrCode::Internal { - internal_message = Some(message); - message = api::ErrCode::Internal.default_public_message().into(); - } - - Err(api::Error { - code, - message, - stack, - internal_message, - details, - }) + Err(coerce_to_api_error(env, val)?) } fn error(&self, _: Env, err: napi::Error) -> Self::Output { diff --git a/runtimes/js/src/lib.rs b/runtimes/js/src/lib.rs index 65c175d240..6c13adaf8f 100644 --- a/runtimes/js/src/lib.rs +++ b/runtimes/js/src/lib.rs @@ -1,6 +1,7 @@ #![deny(clippy::all)] pub mod api; +mod error; mod gateway; mod log; mod meta; diff --git a/runtimes/js/src/pubsub.rs b/runtimes/js/src/pubsub.rs index bdc92e4420..7192ccc274 100644 --- a/runtimes/js/src/pubsub.rs +++ b/runtimes/js/src/pubsub.rs @@ -11,7 +11,7 @@ use encore_runtime_core::pubsub::{SubscriptionObj, TopicObj}; use encore_runtime_core::{api, model, pubsub}; use crate::api::Request; -use crate::log::parse_js_stack; +use crate::error::coerce_to_api_error; use crate::napi_util::{await_promise, PromiseHandler}; use crate::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}; @@ -149,61 +149,8 @@ impl PromiseHandler for SubscriptionPromiseHandler { Ok(()) } - fn reject(&self, env: Env, val: JsUnknown) -> Self::Output { - let obj = val.coerce_to_object().map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - stack: None, - details: None, - })?; - - // Get the details field. - let details: Option = obj - .get_named_property::("details") - .and_then(|val| val.coerce_to_object()) - .and_then(|val| env.from_js_value(val)) - .map(Some) - .unwrap_or(None); - - // Get the message field. - let message: String = obj - .get_named_property::("message") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value(val)) - .map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - stack: None, - details: None, - })?; - - // Get the error code field. - let code: api::ErrCode = obj - .get_named_property::("code") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value::(val)) - .map(|val| { - val.parse::() - .unwrap_or(api::ErrCode::Internal) - }) - .unwrap_or(api::ErrCode::Internal); - - // Get the JS stack - let stack = obj - .get_named_property::("stack") - .and_then(|val| parse_js_stack(&env, val)) - .map(Some) - .unwrap_or(None); - - Err(api::Error { - code, - message, - stack, - internal_message: None, - details, - }) + fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output { + Err(coerce_to_api_error(env, val)?) } fn error(&self, _: Env, err: Error) -> Self::Output { diff --git a/runtimes/js/src/raw_api.rs b/runtimes/js/src/raw_api.rs index 2b52f6faa8..dd2bb75322 100644 --- a/runtimes/js/src/raw_api.rs +++ b/runtimes/js/src/raw_api.rs @@ -15,7 +15,7 @@ use tokio::sync::{mpsc, oneshot}; use encore_runtime_core::api; use crate::api::Request; -use crate::log::parse_js_stack; +use crate::error::coerce_to_api_error; use crate::napi_util::{await_promise, PromiseHandler}; use crate::stream; use crate::threadsafe_function::{ @@ -481,67 +481,8 @@ impl PromiseHandler for RawPromiseHandler { Ok(()) } - fn reject(&self, env: Env, val: JsUnknown) -> Self::Output { - let obj = val.coerce_to_object().map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - stack: None, - details:None, - })?; - - // Get the details field. - let details: Option = obj - .get_named_property::("details") - .and_then(|val| val.coerce_to_object()) - .and_then(|val| env.from_js_value(val)) - .map(Some) - .unwrap_or(None); - - // Get the message field. - let mut message: String = obj - .get_named_property::("message") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value(val)) - .map_err(|_| api::Error { - code: api::ErrCode::Internal, - message: api::ErrCode::Internal.default_public_message().into(), - internal_message: Some("an unknown exception was thrown".into()), - stack: None, - details: None, - })?; - - // Get the error code field. - let code: api::ErrCode = obj - .get_named_property::("code") - .and_then(|val| val.coerce_to_string()) - .and_then(|val| env.from_js_value::(val)) - .map(|val| { - val.parse::() - .unwrap_or(api::ErrCode::Internal) - }) - .unwrap_or(api::ErrCode::Internal); - - // Get the JS stack - let stack = obj - .get_named_property::("stack") - .and_then(|val| parse_js_stack(&env, val)) - .map(Some) - .unwrap_or(None); - - let mut internal_message = None; - if code == api::ErrCode::Internal { - internal_message = Some(message); - message = api::ErrCode::Internal.default_public_message().into(); - } - - Err(api::Error { - code, - message, - stack, - internal_message, - details, - }) + fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output { + Err(coerce_to_api_error(env, val)?) } fn error(&self, _: Env, err: napi::Error) -> Self::Output { From bed0bd0ee123c943cd6ab2962573ba63ab96f3ce Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Wed, 16 Oct 2024 12:11:50 +0200 Subject: [PATCH 3/5] ErrDetails is an object --- runtimes/js/encore.dev/api/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtimes/js/encore.dev/api/error.ts b/runtimes/js/encore.dev/api/error.ts index 3aa7eadfd0..a60a84ab76 100644 --- a/runtimes/js/encore.dev/api/error.ts +++ b/runtimes/js/encore.dev/api/error.ts @@ -114,7 +114,7 @@ export class APIError extends Error { } } -export interface ErrDetails {} +export type ErrDetails = object; export enum ErrCode { /** From 08b78301d16aacffa6a8cd70c5983f1e59ba74d3 Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Wed, 16 Oct 2024 12:33:01 +0200 Subject: [PATCH 4/5] feedback etc --- runtimes/core/src/api/error.rs | 1 + runtimes/js/encore.dev/api/error.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/runtimes/core/src/api/error.rs b/runtimes/core/src/api/error.rs index 924b368993..21bccfebd6 100644 --- a/runtimes/core/src/api/error.rs +++ b/runtimes/core/src/api/error.rs @@ -65,6 +65,7 @@ impl Error { message: ErrCode::Unauthenticated.default_public_message().into(), internal_message: None, stack: None, + details: None, } } } diff --git a/runtimes/js/encore.dev/api/error.ts b/runtimes/js/encore.dev/api/error.ts index a60a84ab76..32c28f8f65 100644 --- a/runtimes/js/encore.dev/api/error.ts +++ b/runtimes/js/encore.dev/api/error.ts @@ -3,7 +3,7 @@ export class APIError extends Error { * The error code. */ public readonly code: ErrCode; - public readonly details?: ErrDetails; + public details?: ErrDetails; // Constructs an APIError with the Canceled error code. static canceled(msg: string, cause?: Error) { @@ -85,6 +85,12 @@ export class APIError extends Error { return new APIError(ErrCode.Unauthenticated, msg, cause); } + // Adds custom details to an error + withDetails(details: ErrDetails): APIError { + this.details = details; + return this; + } + // Constructs an APIError with the given error code, message, and (optionally) cause. constructor(code: ErrCode, msg: string, cause?: Error, details?: ErrDetails) { // extending errors causes issues after you construct them, unless you apply the following fixes @@ -114,7 +120,7 @@ export class APIError extends Error { } } -export type ErrDetails = object; +export type ErrDetails = Record; export enum ErrCode { /** From e40e87ecb2c820cad0ceb90032bfa1d800fc03b7 Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Wed, 16 Oct 2024 13:10:18 +0200 Subject: [PATCH 5/5] construct new error --- runtimes/js/encore.dev/api/error.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/runtimes/js/encore.dev/api/error.ts b/runtimes/js/encore.dev/api/error.ts index 32c28f8f65..289922c7ee 100644 --- a/runtimes/js/encore.dev/api/error.ts +++ b/runtimes/js/encore.dev/api/error.ts @@ -3,7 +3,7 @@ export class APIError extends Error { * The error code. */ public readonly code: ErrCode; - public details?: ErrDetails; + public readonly details?: ErrDetails; // Constructs an APIError with the Canceled error code. static canceled(msg: string, cause?: Error) { @@ -85,10 +85,9 @@ export class APIError extends Error { return new APIError(ErrCode.Unauthenticated, msg, cause); } - // Adds custom details to an error + // Constructs a new APIError from the previous one with the provided details withDetails(details: ErrDetails): APIError { - this.details = details; - return this; + return new APIError(this.code, this.message, this.cause as Error, details); } // Constructs an APIError with the given error code, message, and (optionally) cause.