Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ngrok: parse error responses and surface the msg and code #104

Merged
merged 3 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions ngrok/src/internals/proto.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
collections::HashMap,
error,
fmt,
io,
ops::{
Expand Down Expand Up @@ -38,6 +39,66 @@ pub const SRV_INFO_REQ: StreamType = StreamType::clamp(8);

pub const VERSION: &str = "2";

/// An error that may have an ngrok error code.
/// All ngrok error codes are documented at https://ngrok.com/docs/errors
pub trait NgrokError: error::Error {
/// Return the ngrok error code, if one exists for this error.
fn error_code(&self) -> Option<&str> {
None
}
/// Return the error message minus the ngrok error code.
/// If this error has no error code, this is equivalent to
/// `format!("{error}")`.
fn msg(&self) -> String {
format!("{self}")
}
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ErrResp {
pub msg: String,
pub error_code: Option<String>,
}

impl<'a> From<&'a str> for ErrResp {
fn from(value: &'a str) -> Self {
let mut error_code = None;
let mut msg_lines = vec![];
for line in value.lines().filter(|l| !l.is_empty()) {
if line.starts_with("ERR_NGROK_") {
error_code = line.split('_').nth(2).map(String::from);
} else {
msg_lines.push(line);
}
}
ErrResp {
error_code,
msg: msg_lines.join("\n"),
}
}
}

impl error::Error for ErrResp {}

impl fmt::Display for ErrResp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.msg.fmt(f)?;
if let Some(code) = &self.error_code {
write!(f, "\n\nERR_NGROK_{code}")?;
}
Ok(())
}
}

impl NgrokError for ErrResp {
fn error_code(&self) -> Option<&str> {
self.error_code.as_deref()
}
fn msg(&self) -> String {
self.msg.clone()
}
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Auth {
Expand Down
24 changes: 21 additions & 3 deletions ngrok/src/internals/raw_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ use super::{
BindOpts,
BindResp,
CommandResp,
ErrResp,
NgrokError,
ProxyHeader,
ReadHeaderError,
Restart,
Expand Down Expand Up @@ -95,8 +97,24 @@ pub enum RpcError {
#[error("failed to deserialize rpc response")]
InvalidResponse(#[from] serde_json::Error),
/// There was an error in the RPC response.
#[error("rpc error response: {0}")]
Response(String),
#[error("rpc error response:\n{0}")]
Response(ErrResp),
}

impl NgrokError for RpcError {
fn error_code(&self) -> Option<&str> {
match self {
RpcError::Response(resp) => resp.error_code(),
_ => None,
}
}

fn msg(&self) -> String {
match self {
RpcError::Response(resp) => resp.msg(),
_ => format!("{self}"),
}
}
}

#[derive(Error, Debug)]
Expand Down Expand Up @@ -258,7 +276,7 @@ impl RpcClient {
if let Ok(err) = err_resp {
if !err.error.is_empty() {
debug!(?err, "decoded rpc error response");
return Err(RpcError::Response(err.error));
return Err(RpcError::Response(err.error.as_str().into()));
}
}

Expand Down
1 change: 1 addition & 0 deletions ngrok/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub mod prelude {
#[doc(inline)]
pub use crate::{
config::TunnelBuilder,
internals::proto::NgrokError,
tunnel::{
LabelsTunnel,
ProtoTunnel,
Expand Down
22 changes: 19 additions & 3 deletions ngrok/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ use crate::{
AuthExtra,
BindExtra,
BindOpts,
NgrokError,
SecretString,
},
raw_session::{
Expand Down Expand Up @@ -277,13 +278,13 @@ pub enum ConnectError {
/// This might occur when there's a protocol mismatch interfering with the
/// heartbeat routine.
#[error("failed to start ngrok session")]
Start(StartSessionError),
Start(#[source] StartSessionError),
/// An error occurred when attempting to authenticate.
#[error("authentication failure")]
Auth(RpcError),
Auth(#[source] RpcError),
/// An error occurred when rebinding tunnels during a reconnect
#[error("error rebinding tunnel after reconnect")]
Rebind(RpcError),
Rebind(#[source] RpcError),
/// The (re)connect function gave up.
///
/// This will never be returned by the default connect function, and is
Expand All @@ -292,6 +293,21 @@ pub enum ConnectError {
Canceled,
}

impl NgrokError for ConnectError {
fn error_code(&self) -> Option<&str> {
match self {
ConnectError::Auth(resp) | ConnectError::Rebind(resp) => resp.error_code(),
_ => None,
}
}
fn msg(&self) -> String {
match self {
ConnectError::Auth(resp) | ConnectError::Rebind(resp) => resp.msg(),
_ => format!("{self}"),
}
}
}

impl Default for SessionBuilder {
fn default() -> Self {
SessionBuilder {
Expand Down
Loading