Skip to content

Commit

Permalink
feat(cargo-shuttle): better client errors (#394)
Browse files Browse the repository at this point in the history
* feat(cargo-shuttle): better CLI errors

* refactor: move errorkind to common
  • Loading branch information
oddgrd authored Oct 13, 2022
1 parent 67d0e2e commit b5709fa
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 71 deletions.
20 changes: 13 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ description = "Common library for the shuttle platform (https://www.shuttle.rs/)
chrono = { version = "0.4.22", features = ["serde"] }
comfy-table = { version = "6.1.0", optional = true }
crossterm = { version = "0.25.0", optional = true }
http = { version = "0.2.8", optional = true }
once_cell = "1.13.1"
rustrict = "0.5.0"
serde = { version = "1.0.143", features = ["derive"] }
Expand All @@ -21,5 +22,5 @@ uuid = { version = "1.1.1", features = ["v4", "serde"] }
[features]
default = ["models"]

models = ["display", "serde_json"]
models = ["display", "serde_json", "http"]
display = ["comfy-table", "crossterm"]
71 changes: 70 additions & 1 deletion common/src/models/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,86 @@ use std::fmt::{Display, Formatter};

use comfy_table::Color;
use crossterm::style::Stylize;
use http::StatusCode;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct ApiError {
pub message: String,
pub status_code: u16,
}

impl ApiError {
pub fn status(&self) -> StatusCode {
StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
}

impl Display for ApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message.to_string().with(Color::Red))
write!(
f,
"{}\nmessage: {}",
self.status().to_string().bold(),
self.message.to_string().with(Color::Red)
)
}
}

impl std::error::Error for ApiError {}

#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display)]
pub enum ErrorKind {
KeyMissing,
BadHost,
KeyMalformed,
Unauthorized,
Forbidden,
UserNotFound,
UserAlreadyExists,
ProjectNotFound,
InvalidProjectName,
ProjectAlreadyExists,
ProjectNotReady,
ProjectUnavailable,
InvalidOperation,
Internal,
NotReady,
}

impl From<ErrorKind> for ApiError {
fn from(kind: ErrorKind) -> Self {
let (status, error_message) = match kind {
ErrorKind::Internal => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error"),
ErrorKind::KeyMissing => (StatusCode::UNAUTHORIZED, "request is missing a key"),
ErrorKind::KeyMalformed => (StatusCode::BAD_REQUEST, "request has an invalid key"),
ErrorKind::BadHost => (StatusCode::BAD_REQUEST, "the 'Host' header is invalid"),
ErrorKind::UserNotFound => (StatusCode::NOT_FOUND, "user not found"),
ErrorKind::UserAlreadyExists => (StatusCode::BAD_REQUEST, "user already exists"),
ErrorKind::ProjectNotFound => (
StatusCode::NOT_FOUND,
"project not found. Run `cargo shuttle project new` to create a new project.",
),
ErrorKind::ProjectNotReady => (StatusCode::SERVICE_UNAVAILABLE, "project not ready"),
ErrorKind::ProjectUnavailable => {
(StatusCode::BAD_GATEWAY, "project returned invalid response")
}
ErrorKind::InvalidProjectName => (StatusCode::BAD_REQUEST, "invalid project name"),
ErrorKind::InvalidOperation => (
StatusCode::BAD_REQUEST,
"the requested operation is invalid",
),
ErrorKind::ProjectAlreadyExists => (
StatusCode::BAD_REQUEST,
"a project with the same name already exists",
),
ErrorKind::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"),
ErrorKind::Forbidden => (StatusCode::FORBIDDEN, "forbidden"),
ErrorKind::NotReady => (StatusCode::INTERNAL_SERVER_ERROR, "service not ready"),
};
Self {
message: error_message.to_string(),
status_code: status.as_u16(),
}
}
}
1 change: 1 addition & 0 deletions deployer/src/handlers/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ impl IntoResponse for Error {
)],
Json(ApiError {
message: self.to_string(),
status_code: code.as_u16(),
}),
)
.into_response()
Expand Down
1 change: 0 additions & 1 deletion gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ base64 = "0.13"
bollard = "0.13"
chrono = "0.4"
clap = { version = "4.0.0", features = [ "derive" ] }
convert_case = "0.5.0"
fqdn = "0.2.2"
futures = "0.3.21"
http = "0.2.8"
Expand Down
65 changes: 4 additions & 61 deletions gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ use std::io;
use std::pin::Pin;
use std::str::FromStr;

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use bollard::Docker;
use convert_case::{Case, Casing};
use futures::prelude::*;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use shuttle_common::models::error::ApiError;
use shuttle_common::models::error::{ApiError, ErrorKind};
use tokio::sync::mpsc::error::SendError;
use tracing::error;

Expand All @@ -33,32 +31,6 @@ use crate::service::{ContainerSettings, GatewayService};

static PROJECT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("^[a-zA-Z0-9\\-_]{3,64}$").unwrap());

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
KeyMissing,
BadHost,
KeyMalformed,
Unauthorized,
Forbidden,
UserNotFound,
UserAlreadyExists,
ProjectNotFound,
InvalidProjectName,
ProjectAlreadyExists,
ProjectNotReady,
ProjectUnavailable,
InvalidOperation,
Internal,
NotReady,
}

impl std::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let formatted = format!("{:?}", self).to_case(Case::Snake);
write!(f, "{}", formatted)
}
}

/// Server-side errors that do not have to do with the user runtime
/// should be [`Error`]s.
///
Expand Down Expand Up @@ -116,38 +88,9 @@ impl IntoResponse for Error {
fn into_response(self) -> Response {
error!(error = %self, "request had an error");

let (status, error_message) = match self.kind {
ErrorKind::Internal => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error"),
ErrorKind::KeyMissing => (StatusCode::UNAUTHORIZED, "request is missing a key"),
ErrorKind::KeyMalformed => (StatusCode::BAD_REQUEST, "request has an invalid key"),
ErrorKind::BadHost => (StatusCode::BAD_REQUEST, "the 'Host' header is invalid"),
ErrorKind::UserNotFound => (StatusCode::NOT_FOUND, "user not found"),
ErrorKind::UserAlreadyExists => (StatusCode::BAD_REQUEST, "user already exists"),
ErrorKind::ProjectNotFound => (StatusCode::NOT_FOUND, "project not found"),
ErrorKind::ProjectNotReady => (StatusCode::SERVICE_UNAVAILABLE, "project not ready"),
ErrorKind::ProjectUnavailable => {
(StatusCode::BAD_GATEWAY, "project returned invalid response")
}
ErrorKind::InvalidProjectName => (StatusCode::BAD_REQUEST, "invalid project name"),
ErrorKind::InvalidOperation => (
StatusCode::BAD_REQUEST,
"the requested operation is invalid",
),
ErrorKind::ProjectAlreadyExists => (
StatusCode::BAD_REQUEST,
"a project with the same name already exists",
),
ErrorKind::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"),
ErrorKind::Forbidden => (StatusCode::FORBIDDEN, "forbidden"),
ErrorKind::NotReady => (StatusCode::INTERNAL_SERVER_ERROR, "service not ready"),
};
(
status,
Json(ApiError {
message: error_message.to_string(),
}),
)
.into_response()
let error: ApiError = self.kind.into();

(error.status(), Json(error)).into_response()
}
}

Expand Down

0 comments on commit b5709fa

Please sign in to comment.