Skip to content

Commit ae54fdb

Browse files
authored
Merge pull request #7812 from Turbo87/custom-api-error
util/errors: Extract `CustomApiError` struct
2 parents 2d9f7b9 + 1f749d9 commit ae54fdb

File tree

5 files changed

+45
-190
lines changed

5 files changed

+45
-190
lines changed

src/controllers/metrics.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::controllers::frontend_prelude::*;
2-
use crate::util::errors::{forbidden, not_found, MetricsDisabled};
2+
use crate::util::errors::{custom, forbidden, not_found};
33
use prometheus::TextEncoder;
44

55
/// Handles the `GET /api/private/metrics/:kind` endpoint.
@@ -17,7 +17,8 @@ pub async fn prometheus(app: AppState, Path(kind): Path<String>, req: Parts) ->
1717
} else {
1818
// To avoid accidentally leaking metrics if the environment variable is not set, prevent
1919
// access to any metrics endpoint if the authorization token is not configured.
20-
return Err(Box::new(MetricsDisabled));
20+
let detail = "Metrics are disabled on this crates.io instance";
21+
return Err(custom(StatusCode::NOT_FOUND, detail));
2122
}
2223

2324
let metrics = spawn_blocking(move || match kind.as_str() {

src/middleware/block_traffic.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::app::AppState;
22
use crate::middleware::log_request::RequestLogExt;
33
use crate::middleware::real_ip::RealIp;
4-
use crate::util::errors::RouteBlocked;
4+
use crate::util::errors::custom;
55
use axum::extract::{Extension, MatchedPath, Request};
66
use axum::middleware::Next;
77
use axum::response::{IntoResponse, Response};
@@ -87,7 +87,9 @@ fn rejection_response_from(state: &AppState, headers: &HeaderMap) -> Response {
8787
pub fn block_routes(matched_path: Option<&MatchedPath>, state: &AppState) -> Result<(), Response> {
8888
if let Some(matched_path) = matched_path {
8989
if state.config.blocked_routes.contains(matched_path.as_str()) {
90-
return Err(RouteBlocked.into_response());
90+
let body = "This route is temporarily blocked. See https://status.crates.io.";
91+
let error = custom(StatusCode::SERVICE_UNAVAILABLE, body);
92+
return Err(error.into_response());
9193
}
9294
}
9395

src/models/crate_owner_invitation.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use chrono::{NaiveDateTime, Utc};
22
use diesel::prelude::*;
3+
use http::StatusCode;
34
use secrecy::SecretString;
45

56
use crate::config;
67
use crate::models::{CrateOwner, OwnerKind};
78
use crate::schema::{crate_owner_invitations, crate_owners, crates};
8-
use crate::util::errors::{AppResult, OwnershipInvitationExpired};
9+
use crate::util::errors::{custom, AppResult};
910

1011
#[derive(Debug)]
1112
pub enum NewCrateOwnerInvitationOutcome {
@@ -97,11 +98,17 @@ impl CrateOwnerInvitation {
9798

9899
pub fn accept(self, conn: &mut PgConnection, config: &config::Server) -> AppResult<()> {
99100
if self.is_expired(config) {
100-
let crate_name = crates::table
101+
let crate_name: String = crates::table
101102
.find(self.crate_id)
102103
.select(crates::name)
103104
.first(conn)?;
104-
return Err(Box::new(OwnershipInvitationExpired { crate_name }));
105+
106+
let detail = format!(
107+
"The invitation to become an owner of the {crate_name} crate expired. \
108+
Please reach out to an owner of the crate to request a new invitation.",
109+
);
110+
111+
return Err(custom(StatusCode::GONE, detail));
105112
}
106113

107114
conn.transaction(|conn| {

src/util/errors.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ mod json;
3232
use crate::email::EmailError;
3333
use crates_io_github::GitHubError;
3434
pub use json::TOKEN_FORMAT_ERROR;
35-
pub(crate) use json::{
36-
InsecurelyGeneratedTokenRevoked, MetricsDisabled, OwnershipInvitationExpired, ReadOnlyMode,
37-
RouteBlocked, TooManyRequests,
38-
};
35+
pub(crate) use json::{custom, InsecurelyGeneratedTokenRevoked, ReadOnlyMode, TooManyRequests};
3936

4037
pub type BoxedAppError = Box<dyn AppError>;
4138

@@ -45,7 +42,7 @@ pub type BoxedAppError = Box<dyn AppError>;
4542
/// endpoints, use helpers like `bad_request` or `server_error` which set a
4643
/// correct status code.
4744
pub fn cargo_err<S: ToString>(error: S) -> BoxedAppError {
48-
Box::new(json::Ok(error.to_string()))
45+
custom(StatusCode::OK, error.to_string())
4946
}
5047

5148
// The following are intended to be used for errors being sent back to the Ember
@@ -55,32 +52,35 @@ pub fn cargo_err<S: ToString>(error: S) -> BoxedAppError {
5552

5653
/// Return an error with status 400 and the provided description as JSON
5754
pub fn bad_request<S: ToString>(error: S) -> BoxedAppError {
58-
Box::new(json::BadRequest(error.to_string()))
55+
custom(StatusCode::BAD_REQUEST, error.to_string())
5956
}
6057

6158
pub fn account_locked(reason: &str, until: Option<NaiveDateTime>) -> BoxedAppError {
62-
Box::new(json::AccountLocked {
63-
reason: reason.to_string(),
64-
until,
65-
})
59+
let detail = until
60+
.map(|until| until.format("%Y-%m-%d at %H:%M:%S UTC"))
61+
.map(|until| format!("This account is locked until {until}. Reason: {reason}"))
62+
.unwrap_or_else(|| format!("This account is indefinitely locked. Reason: {reason}"));
63+
64+
custom(StatusCode::FORBIDDEN, detail)
6665
}
6766

6867
pub fn forbidden() -> BoxedAppError {
69-
Box::new(json::Forbidden)
68+
let detail = "must be logged in to perform that action";
69+
custom(StatusCode::FORBIDDEN, detail)
7070
}
7171

7272
pub fn not_found() -> BoxedAppError {
73-
Box::new(json::NotFound)
73+
custom(StatusCode::NOT_FOUND, "Not Found")
7474
}
7575

7676
/// Returns an error with status 500 and the provided description as JSON
7777
pub fn server_error<S: ToString>(error: S) -> BoxedAppError {
78-
Box::new(json::ServerError(error.to_string()))
78+
custom(StatusCode::INTERNAL_SERVER_ERROR, error.to_string())
7979
}
8080

8181
/// Returns an error with status 503 and the provided description as JSON
8282
pub fn service_unavailable() -> BoxedAppError {
83-
Box::new(json::ServiceUnavailable)
83+
custom(StatusCode::SERVICE_UNAVAILABLE, "Service unavailable")
8484
}
8585

8686
// =============================================================================

src/util/errors/json.rs

Lines changed: 14 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use axum::response::{IntoResponse, Response};
22
use axum::Json;
3+
use std::borrow::Cow;
34
use std::fmt;
45

56
use super::{AppError, BoxedAppError, InternalAppErrorStatic};
@@ -16,37 +17,6 @@ fn json_error(detail: &str, status: StatusCode) -> Response {
1617

1718
// The following structs are empty and do not provide a custom message to the user
1819

19-
#[derive(Debug)]
20-
pub(crate) struct NotFound;
21-
22-
impl AppError for NotFound {
23-
fn response(&self) -> Response {
24-
json_error("Not Found", StatusCode::NOT_FOUND)
25-
}
26-
}
27-
28-
impl fmt::Display for NotFound {
29-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30-
"Not Found".fmt(f)
31-
}
32-
}
33-
34-
#[derive(Debug)]
35-
pub(super) struct Forbidden;
36-
37-
impl AppError for Forbidden {
38-
fn response(&self) -> Response {
39-
let detail = "must be logged in to perform that action";
40-
json_error(detail, StatusCode::FORBIDDEN)
41-
}
42-
}
43-
44-
impl fmt::Display for Forbidden {
45-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46-
"must be logged in to perform that action".fmt(f)
47-
}
48-
}
49-
5020
#[derive(Debug)]
5121
pub(crate) struct ReadOnlyMode;
5222

@@ -66,63 +36,28 @@ impl fmt::Display for ReadOnlyMode {
6636

6737
// The following structs wrap owned data and provide a custom message to the user
6838

69-
#[derive(Debug)]
70-
pub(super) struct Ok(pub(super) String);
71-
72-
impl AppError for Ok {
73-
fn response(&self) -> Response {
74-
json_error(&self.0, StatusCode::OK)
75-
}
39+
pub fn custom(status: StatusCode, detail: impl Into<Cow<'static, str>>) -> BoxedAppError {
40+
Box::new(CustomApiError {
41+
status,
42+
detail: detail.into(),
43+
})
7644
}
7745

78-
impl fmt::Display for Ok {
79-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80-
self.0.fmt(f)
81-
}
82-
}
83-
84-
#[derive(Debug)]
85-
pub(super) struct BadRequest(pub(super) String);
86-
87-
impl AppError for BadRequest {
88-
fn response(&self) -> Response {
89-
json_error(&self.0, StatusCode::BAD_REQUEST)
90-
}
46+
#[derive(Debug, Clone)]
47+
pub struct CustomApiError {
48+
status: StatusCode,
49+
detail: Cow<'static, str>,
9150
}
9251

93-
impl fmt::Display for BadRequest {
52+
impl fmt::Display for CustomApiError {
9453
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95-
self.0.fmt(f)
54+
self.detail.fmt(f)
9655
}
9756
}
9857

99-
#[derive(Debug)]
100-
pub(super) struct ServerError(pub(super) String);
101-
102-
impl AppError for ServerError {
58+
impl AppError for CustomApiError {
10359
fn response(&self) -> Response {
104-
json_error(&self.0, StatusCode::INTERNAL_SERVER_ERROR)
105-
}
106-
}
107-
108-
impl fmt::Display for ServerError {
109-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110-
self.0.fmt(f)
111-
}
112-
}
113-
114-
#[derive(Debug)]
115-
pub(crate) struct ServiceUnavailable;
116-
117-
impl AppError for ServiceUnavailable {
118-
fn response(&self) -> Response {
119-
json_error("Service unavailable", StatusCode::SERVICE_UNAVAILABLE)
120-
}
121-
}
122-
123-
impl fmt::Display for ServiceUnavailable {
124-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125-
"Service unavailable".fmt(f)
60+
json_error(&self.detail, self.status)
12661
}
12762
}
12863

@@ -198,93 +133,3 @@ impl fmt::Display for InsecurelyGeneratedTokenRevoked {
198133
Result::Ok(())
199134
}
200135
}
201-
202-
#[derive(Debug)]
203-
pub(super) struct AccountLocked {
204-
pub(super) reason: String,
205-
pub(super) until: Option<NaiveDateTime>,
206-
}
207-
208-
impl AppError for AccountLocked {
209-
fn response(&self) -> Response {
210-
json_error(&self.to_string(), StatusCode::FORBIDDEN)
211-
}
212-
}
213-
214-
impl fmt::Display for AccountLocked {
215-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216-
if let Some(until) = self.until {
217-
let until = until.format("%Y-%m-%d at %H:%M:%S UTC");
218-
write!(
219-
f,
220-
"This account is locked until {}. Reason: {}",
221-
until, self.reason
222-
)
223-
} else {
224-
write!(
225-
f,
226-
"This account is indefinitely locked. Reason: {}",
227-
self.reason
228-
)
229-
}
230-
}
231-
}
232-
233-
#[derive(Debug)]
234-
pub(crate) struct OwnershipInvitationExpired {
235-
pub(crate) crate_name: String,
236-
}
237-
238-
impl AppError for OwnershipInvitationExpired {
239-
fn response(&self) -> Response {
240-
json_error(&self.to_string(), StatusCode::GONE)
241-
}
242-
}
243-
244-
impl fmt::Display for OwnershipInvitationExpired {
245-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246-
write!(
247-
f,
248-
"The invitation to become an owner of the {} crate expired. \
249-
Please reach out to an owner of the crate to request a new invitation.",
250-
self.crate_name
251-
)
252-
}
253-
}
254-
255-
#[derive(Debug)]
256-
pub(crate) struct MetricsDisabled;
257-
258-
impl AppError for MetricsDisabled {
259-
fn response(&self) -> Response {
260-
json_error(&self.to_string(), StatusCode::NOT_FOUND)
261-
}
262-
}
263-
264-
impl fmt::Display for MetricsDisabled {
265-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266-
f.write_str("Metrics are disabled on this crates.io instance")
267-
}
268-
}
269-
270-
#[derive(Debug)]
271-
pub(crate) struct RouteBlocked;
272-
273-
impl AppError for RouteBlocked {
274-
fn response(&self) -> Response {
275-
json_error(&self.to_string(), StatusCode::SERVICE_UNAVAILABLE)
276-
}
277-
}
278-
279-
impl fmt::Display for RouteBlocked {
280-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281-
f.write_str("This route is temporarily blocked. See https://status.crates.io.")
282-
}
283-
}
284-
285-
impl IntoResponse for RouteBlocked {
286-
fn into_response(self) -> Response {
287-
let body = Json(json!({ "errors": [{ "detail": self.to_string() }] }));
288-
(StatusCode::SERVICE_UNAVAILABLE, body).into_response()
289-
}
290-
}

0 commit comments

Comments
 (0)