Skip to content

Cleanup backend json error structs #2625

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

Merged
merged 4 commits into from
Jul 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 4 additions & 5 deletions src/controllers/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::prelude::*;

use crate::middleware::current_user::TrustedUserId;
use crate::models::{ApiToken, User};
use crate::util::errors::{internal, AppError, AppResult, ChainError, Unauthorized};
use crate::util::errors::{forbidden, internal, AppResult, ChainError};

#[derive(Debug)]
pub struct AuthenticatedUser {
Expand All @@ -26,7 +26,7 @@ impl AuthenticatedUser {
}

impl<'a> UserAuthenticationExt for dyn RequestExt + 'a {
/// Obtain `AuthenticatedUser` for the request or return an `Unauthorized` error
/// Obtain `AuthenticatedUser` for the request or return an `Forbidden` error
fn authenticate(&self, conn: &PgConnection) -> AppResult<AuthenticatedUser> {
if let Some(id) = self.extensions().find::<TrustedUserId>() {
// A trusted user_id was provided by a signed cookie (or a test `MockCookieUser`)
Expand All @@ -43,11 +43,10 @@ impl<'a> UserAuthenticationExt for dyn RequestExt + 'a {
token_id: Some(token.id),
})
.chain_error(|| internal("invalid token"))
.chain_error(|| Box::new(Unauthorized) as Box<dyn AppError>)
.chain_error(forbidden)
} else {
// Unable to authenticate the user
Err(internal("no cookie session or auth header found"))
.chain_error(|| Box::new(Unauthorized) as Box<dyn AppError>)
Err(internal("no cookie session or auth header found")).chain_error(forbidden)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::de::DeserializeOwned;
use std::str;

use crate::app::App;
use crate::util::errors::{cargo_err, internal, AppError, AppResult, NotFound};
use crate::util::errors::{cargo_err, internal, not_found, AppError, AppResult};

/// Does all the nonsense for sending a GET to Github. Doesn't handle parsing
/// because custom error-code handling may be desirable. Use
Expand Down Expand Up @@ -46,7 +46,7 @@ fn handle_error_response(app: &App, error: &reqwest::Error) -> Box<dyn AppError>
https://{}/login",
app.config.domain_name,
)),
Some(Status::NOT_FOUND) => Box::new(NotFound),
Some(Status::NOT_FOUND) => not_found(),
_ => internal(&format_args!(
"didn't get a 200 result from github: {}",
error
Expand Down
8 changes: 4 additions & 4 deletions src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ impl Handler for R404 {
req.mut_extensions().insert(m.params.clone());
m.handler.call(req)
}
Err(..) => Ok(NotFound.response().unwrap()),
Err(..) => Ok(NotFound.into()),
}
}
}
Expand All @@ -187,7 +187,7 @@ impl Handler for R404 {
mod tests {
use super::*;
use crate::util::errors::{
bad_request, cargo_err, internal, AppError, ChainError, NotFound, Unauthorized,
bad_request, cargo_err, forbidden, internal, not_found, AppError, ChainError,
};
use crate::util::EndpointResult;

Expand All @@ -209,7 +209,7 @@ mod tests {
StatusCode::BAD_REQUEST
);
assert_eq!(
C(|_| err(Unauthorized)).call(&mut req).unwrap().status(),
C(|_| Err(forbidden())).call(&mut req).unwrap().status(),
StatusCode::FORBIDDEN
);
assert_eq!(
Expand All @@ -220,7 +220,7 @@ mod tests {
StatusCode::NOT_FOUND
);
assert_eq!(
C(|_| err(NotFound)).call(&mut req).unwrap().status(),
C(|_| Err(not_found())).call(&mut req).unwrap().status(),
StatusCode::NOT_FOUND
);

Expand Down
132 changes: 15 additions & 117 deletions src/util/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@ use std::any::{Any, TypeId};
use std::error::Error;
use std::fmt;

use chrono::NaiveDateTime;
use conduit::{header, StatusCode};
use diesel::result::Error as DieselError;

use crate::util::{json_response, AppResponse};
use crate::util::AppResponse;

pub(super) mod concrete;
mod http;
mod json;

pub(crate) use json::{NotFound, ReadOnlyMode, TooManyRequests};

/// Returns an error with status 200 and the provided description as JSON
///
/// This is for backwards compatibility with cargo endpoints. For all other
/// endpoints, use helpers like `bad_request` or `server_error` which set a
/// correct status code.
pub fn cargo_err<S: ToString + ?Sized>(error: &S) -> Box<dyn AppError> {
Box::new(http::Ok(error.to_string()))
Box::new(json::Ok(error.to_string()))
}

// The following are intended to be used for errors being sent back to the Ember
Expand All @@ -41,30 +41,20 @@ pub fn cargo_err<S: ToString + ?Sized>(error: &S) -> Box<dyn AppError> {

/// Return an error with status 400 and the provided description as JSON
pub fn bad_request<S: ToString + ?Sized>(error: &S) -> Box<dyn AppError> {
Box::new(http::BadRequest(error.to_string()))
Box::new(json::BadRequest(error.to_string()))
}

/// Returns an error with status 500 and the provided description as JSON
pub fn server_error<S: ToString + ?Sized>(error: &S) -> Box<dyn AppError> {
Box::new(http::ServerError(error.to_string()))
pub fn forbidden() -> Box<dyn AppError> {
Box::new(json::Forbidden)
}

#[derive(Serialize)]
struct StringError<'a> {
detail: &'a str,
}
#[derive(Serialize)]
struct Bad<'a> {
errors: Vec<StringError<'a>>,
pub fn not_found() -> Box<dyn AppError> {
Box::new(json::NotFound)
}

/// Generates a response with the provided status and description as JSON
fn json_error(detail: &str, status: StatusCode) -> AppResponse {
let mut response = json_response(&Bad {
errors: vec![StringError { detail }],
});
*response.status_mut() = status;
response
/// Returns an error with status 500 and the provided description as JSON
pub fn server_error<S: ToString + ?Sized>(error: &S) -> Box<dyn AppError> {
Box::new(json::ServerError(error.to_string()))
}

// =============================================================================
Expand Down Expand Up @@ -99,7 +89,7 @@ impl dyn AppError {

fn try_convert(err: &(dyn Error + Send + 'static)) -> Option<Box<Self>> {
match err.downcast_ref() {
Some(DieselError::NotFound) => Some(Box::new(NotFound)),
Some(DieselError::NotFound) => Some(not_found()),
Some(DieselError::DatabaseError(_, info))
if info.message().ends_with("read-only transaction") =>
{
Expand Down Expand Up @@ -217,45 +207,6 @@ impl AppError for InternalAppError {
}
}

// TODO: The remaining can probably move under `http`

#[derive(Debug, Clone, Copy)]
pub struct NotFound;

impl From<NotFound> for AppResponse {
fn from(_: NotFound) -> AppResponse {
json_error("Not Found", StatusCode::NOT_FOUND)
}
}

impl AppError for NotFound {
fn response(&self) -> Option<AppResponse> {
Some(NotFound.into())
}
}

impl fmt::Display for NotFound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"Not Found".fmt(f)
}
}

#[derive(Debug, Clone, Copy)]
pub struct Unauthorized;

impl AppError for Unauthorized {
fn response(&self) -> Option<AppResponse> {
let detail = "must be logged in to perform that action";
Some(json_error(detail, StatusCode::FORBIDDEN))
}
}

impl fmt::Display for Unauthorized {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"must be logged in to perform that action".fmt(f)
}
}

pub fn internal<S: ToString + ?Sized>(error: &S) -> Box<dyn AppError> {
Box::new(InternalAppError {
description: error.to_string(),
Expand All @@ -277,59 +228,6 @@ pub(crate) fn std_error(e: Box<dyn AppError>) -> Box<dyn Error + Send> {
Box::new(AppErrToStdErr(e))
}

#[derive(Debug, Clone, Copy)]
pub struct ReadOnlyMode;

impl AppError for ReadOnlyMode {
fn response(&self) -> Option<AppResponse> {
let detail = "Crates.io is currently in read-only mode for maintenance. \
Please try again later.";
Some(json_error(detail, StatusCode::SERVICE_UNAVAILABLE))
}
}

impl fmt::Display for ReadOnlyMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"Tried to write in read only mode".fmt(f)
}
}

#[derive(Debug, Clone, Copy)]
pub struct TooManyRequests {
pub retry_after: NaiveDateTime,
}

impl AppError for TooManyRequests {
fn response(&self) -> Option<AppResponse> {
use std::convert::TryInto;

const HTTP_DATE_FORMAT: &str = "%a, %d %b %Y %H:%M:%S GMT";
let retry_after = self.retry_after.format(HTTP_DATE_FORMAT);

let detail = format!(
"You have published too many crates in a \
short period of time. Please try again after {} or email \
[email protected] to have your limit increased.",
retry_after
);
let mut response = json_error(&detail, StatusCode::TOO_MANY_REQUESTS);
response.headers_mut().insert(
header::RETRY_AFTER,
retry_after
.to_string()
.try_into()
.expect("HTTP_DATE_FORMAT contains invalid char"),
);
Some(response)
}
}

impl fmt::Display for TooManyRequests {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"Too many requests".fmt(f)
}
}

#[test]
fn chain_error_internal() {
assert_eq!(
Expand Down Expand Up @@ -358,7 +256,7 @@ fn chain_error_internal() {
"outer caused by inner"
);
assert_eq!(
Err::<(), _>(Unauthorized)
Err::<(), _>(forbidden())
.chain_error(|| internal("outer"))
.unwrap_err()
.to_string(),
Expand Down
49 changes: 0 additions & 49 deletions src/util/errors/http.rs

This file was deleted.

Loading