diff options
| author | tv <tv@krebsco.de> | 2026-10-26 05:00:06 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-26 05:00:06 +0100 |
| commit | 968a28c6e68e35e127ec3dd20ed2a8eeb8b30cc0 (patch) | |
| tree | 278f93e0a1d008856a7e31e2222e2101e68fdf69 /src | |
| parent | 28400cef36578e5364e8497c9b77618ca0da084b (diff) | |
unify API error responses
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/mod.rs | 64 | ||||
| -rw-r--r-- | src/remake/api.rs | 13 | ||||
| -rw-r--r-- | src/remake/quarantine.rs | 2 |
3 files changed, 62 insertions, 17 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index bec7764..b92f421 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -10,15 +10,17 @@ mod store; use axum::{ Json, Router, - extract::State, + body::Body, + extract::{Request, State}, http::{ HeaderValue, StatusCode, - header::{CONTENT_TYPE, ETAG}, + header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG}, }, + middleware::{self, Next}, response::{IntoResponse, Response}, routing::{delete, get, put}, }; -use serde_json::Value as JsonValue; +use serde_json::{Value as JsonValue, json}; use sha2::{Digest, Sha256}; use tracing::{error, warn}; use utoipa::OpenApi; @@ -61,11 +63,65 @@ pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) -> .route("/debug/blacklist", put(debug::put_debug_blacklist)); } - api.layer(axum::middleware::from_fn_with_state(auth_state, basic_auth)) + api.layer(middleware::from_fn_with_state(auth_state, basic_auth)) .merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", ApiDoc::openapi())) + // Outermost, so authentication failures get the error body too. + .layer(middleware::from_fn(json_errors)) .with_state(state) } +/// An error answer: `{"error": message}` with `details`' fields added (`docs/openapi.yaml`, +/// `Error`). +pub(crate) fn error_response( + status: StatusCode, + message: impl Into<String>, + details: Option<JsonValue>, +) -> Response { + let mut body = json!({ "error": message.into() }); + if let (Some(JsonValue::Object(extra)), Some(object)) = (details, body.as_object_mut()) { + object.extend(extra); + } + (status, Json(body)).into_response() +} + +/// Gives every error answer the same body (`error_response`). Handlers that fail with +/// a bare status or a text message, and axum's own rejections (a malformed query or +/// body), get it here: the text becomes the message, or the status's reason if there +/// is none. Errors that are JSON already pass unchanged, and so do their headers +/// (`WWW-Authenticate` on a 401). +async fn json_errors(request: Request, next: Next) -> Response { + let response = next.run(request).await; + let status = response.status(); + if !(status.is_client_error() || status.is_server_error()) { + return response; + } + let is_json = response + .headers() + .get(CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.starts_with("application/json")); + if is_json { + return response; + } + let (mut parts, body) = response.into_parts(); + // Error texts are short; a larger one is cut to its reason. + let text = axum::body::to_bytes(body, 64 * 1024) + .await + .map(|bytes| String::from_utf8_lossy(&bytes).trim().to_owned()) + .unwrap_or_default(); + let message = if text.is_empty() { + status.canonical_reason().unwrap_or("error").to_lowercase() + } else { + text + }; + let body = json!({ "error": message }).to_string(); + parts.headers.remove(CONTENT_LENGTH); + parts + .headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + Response::from_parts(parts, Body::from(body)) +} + /// `GET /admin/config`: the effective configuration with each value's source; /// imported values are left out. async fn get_config(State(state): State<AppState>) -> Json<JsonValue> { diff --git a/src/remake/api.rs b/src/remake/api.rs index 24fb7e8..515468d 100644 --- a/src/remake/api.rs +++ b/src/remake/api.rs @@ -72,6 +72,7 @@ use super::records::{ Retention, Vote, }; use super::{Host, kind, vote}; +use crate::api::error_response; use crate::document::store::StoreError; use crate::force::Force; @@ -281,18 +282,6 @@ enum StartError { Store(#[from] StoreError), } -pub(super) fn error_response( - status: StatusCode, - message: impl Into<String>, - details: Option<JsonValue>, -) -> Response { - let mut body = json!({ "error": message.into() }); - if let (Some(JsonValue::Object(extra)), Some(object)) = (details, body.as_object_mut()) { - object.extend(extra); - } - (status, Json(body)).into_response() -} - fn grace<H: Host>(host: &H) -> u64 { host.dormant_after().as_millis() as u64 } diff --git a/src/remake/quarantine.rs b/src/remake/quarantine.rs index e7ef136..b5166c9 100644 --- a/src/remake/quarantine.rs +++ b/src/remake/quarantine.rs @@ -27,10 +27,10 @@ use tracing::{info, warn}; use uuid::Uuid; use super::Host; -use super::api::error_response; use super::delta; use super::kind::Translator; use super::local::Quarantined; +use crate::api::error_response; use crate::document::replica::Epoch; use crate::document::store; |
