Центральный обработчик ошибок
Волга предоставляет централизованный механизм обработки ошибок, который перехватывает все ошибки, реализующие типаж Error, возникающие в обработчиках запросов и middleware. Для этого можно воспользоваться методом map_err типа App для регистрации функции, обрабатывающей ошибки.
Функция принимает объект типа Error и должна вернуть ответ, реализующий типаж IntoResponse.
Пример:
use volga::{App, error::Error, status};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/error", || async {
std::io::Error::other("some error")
});
// Регистрируем централизованный обработчик ошибок
app.map_err(|error: Error| async move {
status!(500, "{:?}", error)
});
app.run().await
}
В этом примере мы намеренно создаем обработчик запросов, который выдает ошибку, и определяем обработчик ошибок, который генерирует HTTP-ответ с кодом состояния 500 на основе сообщения об ошибке.
Для удобства структура Error уже включает поле status, которое охватывает общие случаи (400, 401, 403, 404 и т. д.), что позволяет использовать макрос следующим образом:
status!(error.status.as_u16(), "{:?}", error)
Фактически, именно так реализован обработчик ошибок по умолчанию. Если мы удалим метод map_err, ответ останется неизменным. Однако переопределение пользовательского обработчика ошибок обеспечивает большую гибкость для логирования и трассировки.
Problem Details
Волга полностью поддерживает формат Problem Details, который предоставляет машиночитаемые сведения об ошибках в ответах HTTP. Это устраняет необходимость определять пользовательские форматы ошибок для API.
Чтобы включить эту возможность, убедитесь, что функция problem-details активирована в Cargo.toml вашего приложения:
[dependencies]
volga = { version = "...", features = ["problem-details"] }
Затем вы можете вернуть структуру Problem из обработчика запроса:
use volga::{App, error::Problem};
use serde::Serialize;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/problem", || async {
// Всегда выдает Problem Details
Problem::new(400)
.with_detail("Missing Parameter")
.with_instance("/problem")
.with_extensions(ValidationError {
invalid_params: vec![InvalidParam {
name: "id".into(),
reason: "The ID must be provided".into()
}]
})
});
app.run().await
}
#[derive(Default, Serialize)]
struct ValidationError {
#[serde(rename = "invalid-params")]
invalid_params: Vec<InvalidParam>,
}
#[derive(Default, Serialize)]
struct InvalidParam {
name: String,
reason: String,
}
Пример ответа:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"detail": "Missing Parameter",
"instance": "/problem",
"invalid-params": [
{ "name": "id", "reason": "The ID must be provided" }
]
}
Центральная обработка ошибок с Problem Details
Кроме того, вы можете комбинировать Problem с map_err, используя метод use_problem_details():
use volga::{App, error::Error};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/error", || async {
// Всегда выдает ошибку, которая будет преобразована
// в Problem Details
std::io::Error::other("some error")
});
// Регистрируем централизованный обработчик ошибок, который выдает
// ответы в формате Problem Details
app.use_problem_details();
app.run().await
}
Пример ответа:
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
"title": "Internal Server Error",
"status": 500,
"detail": "some error",
"instance": "/error"
}
Поля type и title определяются из кода состояния, но могут быть переопределены:
Problem::new(400)
.with_type("https://tools.ietf.org/html/rfc9110#section-15.6.1")
.with_title("Server Error");
А также при необходимости можно добавить дополнительные сведения:
Problem::new(400)
.with_detail("Missing Parameter")
.with_instance("/problem")
.with_extensions(ValidationError {
invalid_params: vec![InvalidParam {
name: "id".into(),
reason: "The ID must be provided".into()
}]
})
или
Problem::new(400)
.with_detail("Missing Parameter")
.with_instance("/problem")
.add_param("reason", "The ID must be provided");
Готовые примеры можно найти по следующим ссылкам: