Jump to…
snowinitial commitqoxwzsukwmkx1mo
1//! Request error handling.
2//!
3//! The important rule (spec §9): an unauthorised private repo and a nonexistent
4//! repo must be indistinguishable. [`AppError::NotFound`] is therefore the only
5//! way to say "you can't see this" — there is deliberately no `Forbidden`
6//! variant for repository access.
7
8use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10
11#[derive(Debug)]
12pub enum AppError {
13 /// Also the response for "exists but you may not see it".
14 NotFound,
15 BadRequest(String),
16 /// Signed in, but lacking a permission that is not itself a secret —
17 /// e.g. trying to change settings on a repo you can already read.
18 Forbidden,
19 /// Not signed in, on a page that requires it.
20 Unauthorized,
21 Internal(anyhow::Error),
22}
23
24impl std::fmt::Display for AppError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 AppError::NotFound => write!(f, "not found"),
28 AppError::BadRequest(m) => write!(f, "bad request: {m}"),
29 AppError::Forbidden => write!(f, "forbidden"),
30 AppError::Unauthorized => write!(f, "unauthorized"),
31 AppError::Internal(e) => write!(f, "internal error: {e}"),
32 }
33 }
34}
35
36impl<E: Into<anyhow::Error>> From<E> for AppError {
37 fn from(e: E) -> Self {
38 AppError::Internal(e.into())
39 }
40}
41
42impl IntoResponse for AppError {
43 fn into_response(self) -> Response {
44 let (status, message) = match self {
45 AppError::NotFound => (StatusCode::NOT_FOUND, "Not found".to_string()),
46 AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
47 AppError::Forbidden => (
48 StatusCode::FORBIDDEN,
49 "You do not have permission to do that.".to_string(),
50 ),
51 AppError::Unauthorized => {
52 // Send them to sign in rather than rendering a dead end.
53 return axum::response::Redirect::to("/login").into_response();
54 }
55 AppError::Internal(e) => {
56 // Log the detail; never show it. Internal errors routinely carry
57 // connection strings and object ids.
58 tracing::error!("internal error: {e:#}");
59 // In tests the detail goes to stderr as well. A failing security
60 // test that can only report "500" is nearly useless, and this
61 // path is compiled out of every real build.
62 #[cfg(test)]
63 eprintln!("internal error: {e:#}");
64 (
65 StatusCode::INTERNAL_SERVER_ERROR,
66 "Something went wrong.".to_string(),
67 )
68 }
69 };
70
71 (status, crate::views::layout::bare_error(status, &message)).into_response()
72 }
73}
74
75pub type AppResult<T> = Result<T, AppError>;

75 lines · Rust