From bcf0ae159d949dc268657c163b0d87defe7f05d2 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 24 Feb 2024 12:41:43 -0800 Subject: [PATCH] Better gRPC status on error --- src-tauri/grpc/src/manager.rs | 46 ++++-- src-tauri/src/main.rs | 137 ++++++++++-------- src-web/components/EmptyStateText.tsx | 11 +- .../components/GrpcConnectionMessagesPane.tsx | 44 ++++-- src-web/components/core/KeyValueRow.tsx | 6 +- 5 files changed, 155 insertions(+), 89 deletions(-) diff --git a/src-tauri/grpc/src/manager.rs b/src-tauri/grpc/src/manager.rs index 55f56baf..ca7ef7a4 100644 --- a/src-tauri/grpc/src/manager.rs +++ b/src-tauri/grpc/src/manager.rs @@ -25,6 +25,30 @@ pub struct GrpcConnection { pub uri: Uri, } +#[derive(Default)] +pub struct StreamError { + pub message: String, + pub status: Option, +} + +impl From for StreamError { + fn from(value: String) -> Self { + StreamError { + message: value.to_string(), + status: None, + } + } +} + +impl From for StreamError { + fn from(s: Status) -> Self { + StreamError { + message: s.message().to_string(), + status: Some(s), + } + } +} + impl GrpcConnection { pub fn service(&self, service: &str) -> Result { let service = self @@ -49,7 +73,7 @@ impl GrpcConnection { method: &str, message: &str, metadata: HashMap, - ) -> Result, String> { + ) -> Result, StreamError> { let method = &self.method(&service, &method)?; let input_message = method.input(); @@ -67,10 +91,7 @@ impl GrpcConnection { let codec = DynamicCodec::new(method.clone()); client.ready().await.unwrap(); - client - .unary(req, path, codec) - .await - .map_err(|e| e.to_string()) + Ok(client.unary(req, path, codec).await?) } pub async fn streaming( @@ -79,7 +100,7 @@ impl GrpcConnection { method: &str, stream: ReceiverStream, metadata: HashMap, - ) -> Result>, Status>, String> { + ) -> Result>, StreamError> { let method = &self.method(&service, &method)?; let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); @@ -90,7 +111,7 @@ impl GrpcConnection { let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); client.ready().await.map_err(|e| e.to_string())?; - Ok(client.streaming(req, path, codec).await) + Ok(client.streaming(req, path, codec).await?) } pub async fn client_streaming( @@ -99,7 +120,7 @@ impl GrpcConnection { method: &str, stream: ReceiverStream, metadata: HashMap, - ) -> Result, String> { + ) -> Result, StreamError> { let method = &self.method(&service, &method)?; let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); let mut req = stream.into_streaming_request(); @@ -111,7 +132,10 @@ impl GrpcConnection { client .client_streaming(req, path, codec) .await - .map_err(|s| s.to_string()) + .map_err(|e| StreamError { + message: e.message().to_string(), + status: Some(e), + }) } pub async fn server_streaming( @@ -120,7 +144,7 @@ impl GrpcConnection { method: &str, message: &str, metadata: HashMap, - ) -> Result>, Status>, String> { + ) -> Result>, StreamError> { let method = &self.method(&service, &method)?; let input_message = method.input(); @@ -137,7 +161,7 @@ impl GrpcConnection { let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); client.ready().await.map_err(|e| e.to_string())?; - Ok(client.server_streaming(req, path, codec).await) + Ok(client.server_streaming(req, path, codec).await?) } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1e7ec010..6f13ac4f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,52 +10,52 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; -use std::fs::{create_dir_all, File, read_to_string}; +use std::fs::{create_dir_all, read_to_string, File}; use std::path::PathBuf; use std::process::exit; use std::str::FromStr; -use ::http::Uri; use ::http::uri::InvalidUri; +use ::http::Uri; use base64::Engine; use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; use serde_json::{json, Value}; -use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; use sqlx::types::Json; -use tauri::{AppHandle, RunEvent, State, Window, WindowUrl}; -use tauri::{Manager, WindowEvent}; +use sqlx::{Pool, Sqlite, SqlitePool}; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; +use tauri::{AppHandle, RunEvent, State, Window, WindowUrl}; +use tauri::{Manager, WindowEvent}; use tauri_plugin_log::{fern, LogTarget}; use tauri_plugin_window_state::{StateFlags, WindowExt}; -use tokio::sync::{Mutex}; +use tokio::sync::Mutex; use tokio::time::sleep; use window_shadows::set_shadow; -use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition}; use ::grpc::manager::{DynamicMessage, GrpcHandle}; +use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition}; use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::grpc::metadata_to_map; use crate::http::send_http_request; use crate::models::{ - cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, - create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, - delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, - delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, - duplicate_http_request, Environment, EnvironmentVariable, Folder, get_cookie_jar, - get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, - get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, - get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, - HttpRequest, HttpResponse, KeyValue, list_cookie_jars, list_environments, - list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, - list_requests, list_responses, list_workspaces, set_key_value_raw, Settings, - update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, - upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, WorkspaceExportResources, + cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, + delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, + delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, + delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, + get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, + get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, + get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, + list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses, + list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, + upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, + upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment, + EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, + HttpRequest, HttpResponse, KeyValue, Settings, Workspace, WorkspaceExportResources, }; use crate::plugin::ImportResult; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; @@ -454,16 +454,26 @@ async fn cmd_grpc_go( Some(Err(e)) => { upsert_grpc_event( &w, - &GrpcEvent { - content: "Failed to connect".to_string(), - event_type: GrpcEventType::ConnectionEnd, - error: Some(e.to_string()), - status: Some(Code::Unknown as i64), - ..base_event.clone() - }, + &(match e.status { + Some(s) => GrpcEvent { + error: Some(s.message().to_string()), + status: Some(s.code() as i64), + content: "Failed to connect".to_string(), + metadata: Json(metadata_to_map(s.metadata().clone())), + event_type: GrpcEventType::ConnectionEnd, + ..base_event.clone() + }, + None => GrpcEvent { + error: Some(e.message), + status: Some(Code::Unknown as i64), + content: "Failed to connect".to_string(), + event_type: GrpcEventType::ConnectionEnd, + ..base_event.clone() + }, + }), ) - .await - .unwrap(); + .await + .unwrap(); } None => { // Server streaming doesn't return initial message @@ -471,7 +481,7 @@ async fn cmd_grpc_go( } let mut stream = match maybe_stream { - Some(Ok(Ok(stream))) => { + Some(Ok(stream)) => { upsert_grpc_event( &w, &GrpcEvent { @@ -490,31 +500,26 @@ async fn cmd_grpc_go( .unwrap(); stream.into_inner() } - Some(Ok(Err(e))) => { - upsert_grpc_event( - &w, - &GrpcEvent { - error: Some(e.message().to_string()), - status: Some(e.code() as i64), - content: e.code().description().to_string(), - event_type: GrpcEventType::ConnectionEnd, - ..base_event.clone() - }, - ) - .await - .unwrap(); - return; - } Some(Err(e)) => { upsert_grpc_event( &w, - &GrpcEvent { - error: Some(e), - status: Some(Code::Unknown as i64), - content: "Unknown error".to_string(), - event_type: GrpcEventType::ConnectionEnd, - ..base_event.clone() - }, + &(match e.status { + Some(s) => GrpcEvent { + error: Some(s.message().to_string()), + status: Some(s.code() as i64), + content: "Failed to connect".to_string(), + metadata: Json(metadata_to_map(s.metadata().clone())), + event_type: GrpcEventType::ConnectionEnd, + ..base_event.clone() + }, + None => GrpcEvent { + error: Some(e.message), + status: Some(Code::Unknown as i64), + content: "Failed to connect".to_string(), + event_type: GrpcEventType::ConnectionEnd, + ..base_event.clone() + }, + }), ) .await .unwrap(); @@ -655,9 +660,12 @@ async fn cmd_send_ephemeral_request( }; let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); - window.listen_global(format!("cancel_http_response_{}", response.id), move |_event| { - let _ = cancel_tx.send(true); - }); + window.listen_global( + format!("cancel_http_response_{}", response.id), + move |_event| { + let _ = cancel_tx.send(true); + }, + ); send_http_request( &window, @@ -860,9 +868,12 @@ async fn cmd_send_http_request( }; let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); - window.listen_global(format!("cancel_http_response_{}", response.id), move |_event| { - let _ = cancel_tx.send(true); - }); + window.listen_global( + format!("cancel_http_response_{}", response.id), + move |_event| { + let _ = cancel_tx.send(true); + }, + ); send_http_request( &window, @@ -905,9 +916,15 @@ async fn cmd_track_event( analytics::track_event(&window.app_handle(), resource, action, attributes).await; } (r, a) => { - println!("HttpRequest: {:?}", serde_json::to_string(&AnalyticsResource::HttpRequest)); + println!( + "HttpRequest: {:?}", + serde_json::to_string(&AnalyticsResource::HttpRequest) + ); println!("Send: {:?}", serde_json::to_string(&AnalyticsAction::Send)); - error!("Invalid action/resource for track_event: {resource}.{action} = {:?}.{:?}", r, a); + error!( + "Invalid action/resource for track_event: {resource}.{action} = {:?}.{:?}", + r, a + ); return Err("Invalid event".to_string()); } }; diff --git a/src-web/components/EmptyStateText.tsx b/src-web/components/EmptyStateText.tsx index 2ad80c11..348dd9a3 100644 --- a/src-web/components/EmptyStateText.tsx +++ b/src-web/components/EmptyStateText.tsx @@ -1,12 +1,19 @@ +import classNames from 'classnames'; import type { ReactNode } from 'react'; interface Props { children: ReactNode; + className?: string; } -export function EmptyStateText({ children }: Props) { +export function EmptyStateText({ children, className }: Props) { return ( -
+
{children}
); diff --git a/src-web/components/GrpcConnectionMessagesPane.tsx b/src-web/components/GrpcConnectionMessagesPane.tsx index ab591413..c1fbb098 100644 --- a/src-web/components/GrpcConnectionMessagesPane.tsx +++ b/src-web/components/GrpcConnectionMessagesPane.tsx @@ -106,20 +106,29 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: ) : (
-
- {activeEvent.error ?? activeEvent.content} +
+
+ {activeEvent.content} +
+ {activeEvent.error && ( +
+ {activeEvent.error} +
+ )} +
+
+ {Object.keys(activeEvent.metadata).length === 0 ? ( + + No {activeEvent.eventType === 'connection_end' ? 'trailers' : 'metadata'} + + ) : ( + + {Object.entries(activeEvent.metadata).map(([key, value]) => ( + + ))} + + )}
- {Object.keys(activeEvent.metadata).length === 0 ? ( - - No {activeEvent.eventType === 'connection_end' ? 'trailers' : 'metadata'} - - ) : ( - - {Object.entries(activeEvent.metadata).map(([key, value]) => ( - - ))} - - )}
)}
@@ -186,7 +195,14 @@ function EventRow({ : 'info' } /> -
{error ?? content}
+
+ {content} + {error && ( + <> + ({error}) + + )} +
{format(createdAt + 'Z', 'HH:mm:ss.SSS')}
diff --git a/src-web/components/core/KeyValueRow.tsx b/src-web/components/core/KeyValueRow.tsx index 0847ba01..7c7a8176 100644 --- a/src-web/components/core/KeyValueRow.tsx +++ b/src-web/components/core/KeyValueRow.tsx @@ -29,10 +29,12 @@ interface Props { export function KeyValueRow({ label, value, labelClassName }: Props) { return ( <> - + {label} - {value} + {value} ); }