A set of extensions for building web applications on axum.
Unstable: This crate is not ready for use. The author is building out these extensions to iterate on a proof of concept, and the surface may change frequently.
The api::responses module provides a set of response conventions for axum handlers, implementing axum's IntoResponse trait for typical use cases.
A handler returns JsonResult<T>:
// ...
use omnium::api::{JsonResult, JsonResponse, JsonStatus};
async fn handler() -> JsonResult<JsonStatus> {
let result = try_do_or_err().await;
match result {
Ok => JsonResponse::of_status(StatusCode::ACCEPTED).into()
Err => JsonResponse::of_status(StatusCode::CONFLICT).into()
}
}You can build responses using JsonResponse<T>, which implements Into<JsonResult>, as well as axum's IntoResponse.
For clarity, you can use the provided respond! macro instead of calling .into().
A handler can return a JSON response for any serializable body, with a default OK status:
async fn handler() -> JsonResult<SomeBodyType> {
respond!(JsonResponse::of(body));
}Another status code can be set on the response:
async fn handler() -> JsonResult<JsonStatus> {
respond!(JsonResponse::of(body).with_status(StatusCode::IM_A_TEAPOT));
}A handler can return a simple JsonStatus status response, implicitly deriving the response body as appropriate for the status:
async fn handler() -> JsonResult<JsonStatus> {
respond!(JsonResponse::of_status(StatusCode::OK));
}An additional detail message can be added to the JsonStatus:
async fn handler() -> JsonResult<JsonStatus> {
respond!(JsonResponse::of_status(StatusCode::OK).with_detail("Additional detail"));
}A status response can be returned on the Err arm of JsonResult, regardless of the happy path response type.
async fn handler() -> JsonResult<()> {
Err(JsonResponse::of_status(StatusCode::OK).with_detail("Additional detail"))
}An arbitrary error propagated to the top of a handler is treated as an internal server error, and is rendered with an opaque INTERNAL_SERVER_ERROR response that masks any error details from the caller.
async fn handler() -> JsonResult<()> {
let success = try_do_or_err().await?;
// ...
}Run your application with the RUST_BACKTRACE=1 environment variable set for such an internal error to display a backtrace when logged.
Depending on whether you are in a function returning anyhow::Result or JsonResult, you can return errors using anyhow::bail! or respond_err!, respectively. In both cases, you can propagate an arbitrary error to be handled as an internal server error, or you can provide a JsonResult status response.
async fn handler() -> anyhow::Result<()> {
anyhow::bail!("Internal error!");
// ...
}async fn handler() -> JsonResult<()> {
respond_err!("Internal error!");
// ...
}The session module provides JWT-based authentication middleware, with utilities for a cookie-based credential exchange or the authorization header for browser-based or programmatic authentication.
Configure authentication middleware:
struct UserAccount {}
struct AppState {}
#[async_trait]
impl SessionManager<UserAccount> for Arc<AppState> {
fn decode_claims(&self, credential: Credential) -> anyhow::Result<SessionClaims> {
// Decode claims from credential...
}
async fn get_account(&self, account_id: String) -> anyhow::Result<Option<UserAccount>> {
// Return account from application database...
}
fn extract_credential(&self, request: &Request, _cookies: &CookieJar) -> Option<Credential> {
// Extract credential from request...
}
}Attach authentication middleware:
fn app(state: Arc<AppState>) -> Router {
Router::new()
.route("/api/account", get(|| async { "Hello, caller!" }))
.layer(from_fn_with_state(
state.clone(),
authorize::<UserAccount, Arc<AppState>>,
))
.layer(from_fn_with_state(
state.clone(),
resolve::<UserAccount, Arc<AppState>>,
))
.with_state(state)
}Note that the authentication middleware is attached in two parts: 1) resolving the authenticated account on the request, and 2) enforcing authentication for the request. You can layer these middleware components to support account-aware routes for your entire application, while a subset of the application requires authorizing.
Create the account session:
create_session(
"some-account-id",
&EncodingKey::from_secret(...),
Duration::from_secs(60),
);With Credential::from_authorization_header, a client may pass the session as the authorization header. With Credential::from_cookie, a client may pass the session as a cookie. For a simple, user-facing web application, you can set a __Host- cookie when the account signs in, in order to authenticate requests to the service running on the same origin. If the application shares a session across multiple services on different origins, it might expose the session for use by the client in the authorization header for programmatic or cross-origin requests. You can plug in your own handling for extracting credentials from requests with a custom extract_credential handler.
Requests from an unauthenticated caller will reject with a 401 response.
For an authenticated caller, the account object from get_account can be used throughout the request lifecycle to avoid redundant lookup in handlers:
pub async fn handler(
Extension(caller): Extension<AppAccount>,
) {
println!("Caller is: {}", caller);
}