1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
//! Request-bound [SQLx] transactions for [axum].
//!
//! [SQLx]: https://github.com/launchbadge/sqlx#readme
//! [axum]: https://github.com/tokio-rs/axum#readme
//!
//! [`Tx`] is an `axum` [extractor][axum extractors] for obtaining a transaction that's bound to the
//! HTTP request. A transaction begins the first time the extractor is used for a request, and is
//! then stored in [request extensions] for use by other middleware/handlers. The transaction is
//! resolved depending on the status code of the eventual response – successful (HTTP `2XX` or
//! `3XX`) responses will cause the transaction to be committed, otherwise it will be rolled back.
//!
//! This behaviour is often a sensible default, and using the extractor (e.g. rather than directly
//! using [`sqlx::Transaction`]s) means you can't forget to commit the transactions!
//!
//! [axum extractors]: https://docs.rs/axum/latest/axum/#extractors
//! [request extensions]: https://docs.rs/http/latest/http/struct.Extensions.html
//!
//! # Usage
//!
//! To use the [`Tx`] extractor, you must first add [`Layer`] to your app:
//!
//! ```
//! # async fn foo() {
//! let pool = /* any sqlx::Pool */
//! # sqlx::SqlitePool::connect(todo!()).await.unwrap();
//! let app = axum::Router::new()
//! // .route(...)s
//! .layer(axum_sqlx_tx::Layer::new(pool));
//! # axum::Server::bind(todo!()).serve(app.into_make_service());
//! # }
//! ```
//!
//! You can then simply add [`Tx`] as an argument to your handlers:
//!
//! ```
//! use axum_sqlx_tx::Tx;
//! use sqlx::Sqlite;
//!
//! async fn create_user(mut tx: Tx<Sqlite>, /* ... */) {
//! // `&mut Tx` implements `sqlx::Executor`
//! let user = sqlx::query("INSERT INTO users (...) VALUES (...)")
//! .fetch_one(&mut tx)
//! .await
//! .unwrap();
//!
//! // `Tx` also implements `Deref<Target = sqlx::Transaction>` and `DerefMut`
//! use sqlx::Acquire;
//! let inner = tx.begin().await.unwrap();
//! /* ... */
//! }
//! ```
//!
//! If you forget to add the middleware you'll get [`Error::MissingExtension`] (internal server
//! error) when using the extractor. You'll also get an error ([`Error::OverlappingExtractors`]) if
//! you have multiple `Tx` arguments in a single handler, or call `Tx::from_request` multiple times
//! in a single middleware.
//!
//! ## Error handling
//!
//! `axum` requires that middleware do not return errors, and that the errors returned by extractors
//! implement `IntoResponse`. By default, [`Error`](Error) is used by [`Layer`] and [`Tx`] to
//! convert errors into HTTP 500 responses, with the error's `Display` value as the response body,
//! however it's generally not a good practice to return internal error details to clients!
//!
//! To make it easier to customise error handling, both [`Layer`] and [`Tx`] have a second generic
//! type parameter, `E`, that can be used to override the error type that will be used to convert
//! the response.
//!
//! ```
//! use axum::response::IntoResponse;
//! use axum_sqlx_tx::Tx;
//! use sqlx::Sqlite;
//!
//! struct MyError(axum_sqlx_tx::Error);
//!
//! // Errors must implement From<axum_sqlx_tx::Error>
//! impl From<axum_sqlx_tx::Error> for MyError {
//! fn from(error: axum_sqlx_tx::Error) -> Self {
//! Self(error)
//! }
//! }
//!
//! // Errors must implement IntoResponse
//! impl IntoResponse for MyError {
//! fn into_response(self) -> axum::response::Response {
//! // note that you would probably want to log the error or something
//! (http::StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
//! }
//! }
//!
//! // Change the layer error type
//! # async fn foo() {
//! # let pool: sqlx::SqlitePool = todo!();
//! let app = axum::Router::new()
//! // .route(...)s
//! .layer(axum_sqlx_tx::Layer::new_with_error::<MyError>(pool));
//! # axum::Server::bind(todo!()).serve(app.into_make_service());
//! # }
//!
//! // Change the extractor error type
//! async fn create_user(mut tx: Tx<Sqlite, MyError>, /* ... */) {
//! /* ... */
//! }
//! ```
//!
//! # Examples
//!
//! See [`examples/`][examples] in the repo for more examples.
//!
//! [examples]: https://github.com/wasdacraic/axum-sqlx-tx/tree/master/examples
#![cfg_attr(doc, deny(warnings))]
mod layer;
mod slot;
mod tx;
pub use crate::{
layer::{Layer, Service},
tx::Tx,
};
/// Possible errors when extracting [`Tx`] from a request.
///
/// `axum` requires that the `FromRequest` `Rejection` implements `IntoResponse`, which this does
/// by returning the `Display` representation of the variant. Note that this means returning
/// configuration and database errors to clients, but you can override the type of error that
/// `Tx::from_request` returns using the `E` generic parameter:
///
/// ```
/// use axum::response::IntoResponse;
/// use axum_sqlx_tx::Tx;
/// use sqlx::Sqlite;
///
/// struct MyError(axum_sqlx_tx::Error);
///
/// // The error type must implement From<axum_sqlx_tx::Error>
/// impl From<axum_sqlx_tx::Error> for MyError {
/// fn from(error: axum_sqlx_tx::Error) -> Self {
/// Self(error)
/// }
/// }
///
/// // The error type must implement IntoResponse
/// impl IntoResponse for MyError {
/// fn into_response(self) -> axum::response::Response {
/// (http::StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
/// }
/// }
///
/// async fn handler(tx: Tx<Sqlite, MyError>) {
/// /* ... */
/// }
/// ```
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Indicates that the [`Layer`](crate::Layer) middleware was not installed.
#[error("required extension not registered; did you add the axum_sqlx_tx::Layer middleware?")]
MissingExtension,
/// Indicates that [`Tx`] was extracted multiple times in a single handler/middleware.
#[error("axum_sqlx_tx::Tx extractor used multiple times in the same handler/middleware")]
OverlappingExtractors,
/// A database error occurred when starting the transaction.
#[error(transparent)]
Database {
#[from]
error: sqlx::Error,
},
}
impl axum_core::response::IntoResponse for Error {
fn into_response(self) -> axum_core::response::Response {
(http::StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
}
}