# tower-conneg
Tower middleware for HTTP content negotiation on both client and server.
## Features
- **Server-side**: Deserialize request bodies via `Content-Type`, serialize responses via `Accept`
- **Client-side**: Serialize request bodies, deserialize responses via `Content-Type`
- **Multiple formats**: JSON, `MessagePack`, CBOR, XML, TOML, Form, Postcard, BSON, plain text
- **Framework integrations**: Axum, Poem, Salvo, Viz
- **`OpenAPI` helpers**: Utoipa request bodies, responses, and negotiation errors
- **Type-safe**: Explicit format threading through handler signatures
- **Serde-based**: Uses `erased-serde` for type erasure at the format level
## Quick Start (Server with Axum)
```rust,ignore
use axum::{Router, routing::post};
use tower_conneg::{Negotiate, NegotiateResponse, NegotiateLayer, ServerConfig, JsonFormat};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
async fn create_user(req: Negotiate<CreateUser>) -> NegotiateResponse<User> {
let user = User { id: 1, name: req.name.clone() };
req.respond(user)
}
let config = ServerConfig::builder()
.formats([JsonFormat])
.build();
let app = Router::new()
.route("/users", post(create_user))
.layer(NegotiateLayer::new(config));
```
For endpoints without request bodies (GET, DELETE), use `Negotiate<()>`:
```rust,ignore
async fn get_user(neg: Negotiate<()>, Path(id): Path<u64>) -> NegotiateResponse<User> {
let user = db.get(id).await;
neg.respond(user)
}
```
## Quick Start (Client)
```rust,ignore
use tower_conneg::{ClientNegotiateLayer, ClientConfig, JsonFormat, MsgPackFormat};
let config = ClientConfig::builder()
.formats([MsgPackFormat, JsonFormat])
.fallback_format(JsonFormat)
.build();
let client = ServiceBuilder::new()
.layer(ClientNegotiateLayer::new(config))
.service(hyper_client);
```
## Available Formats
| JSON | `json` (default) | `application/json` |
| `MessagePack` | `msgpack` | `application/msgpack` |
| CBOR | `cbor` | `application/cbor` |
| XML | `xml` | `application/xml` |
| TOML | `toml` | `application/toml` |
| Form | `form` | `application/x-www-form-urlencoded` |
| Postcard | `postcard` | `application/x-postcard` |
| BSON | `bson` | `application/bson` |
| Plain Text | `plain` | `text/plain` |
| HTML | `plain` | `text/html` |
## Framework Integrations
| Axum | `axum` | `Negotiate<T>` implements `FromRequest`/`FromRequestParts`, `NegotiateResponse` implements `IntoResponse` |
| Poem | `poem` | Native extractor/responder support |
| Salvo | `salvo` | Native `Extractible` support |
| Viz | `viz` | Native extractor support |
| Hyper Client | `hyper-client` | Client-side negotiation |
| Utoipa | `utoipa` | `OpenAPI` content, request body, response, and error helpers |
## `OpenAPI` with Utoipa
Enable the `utoipa` feature to document the same negotiated media types your handlers accept and
return:
```rust
use tower_conneg::OpenApiFormats;
use utoipa::openapi::{
HttpMethod, PathItem, PathsBuilder, path::OperationBuilder,
};
use utoipa::ToSchema;
#[derive(ToSchema)]
struct CreateUserRequest {
name: String,
email: String,
}
#[derive(ToSchema)]
struct User {
id: u64,
name: String,
email: String,
}
let docs = OpenApiFormats::from_media_types([
"application/json",
"application/msgpack",
"application/x-msgpack",
]);
let create_user = OperationBuilder::new()
.request_body(Some(docs.required_request_body::<CreateUserRequest>()))
.response("201", docs.response::<User, _>("Created user."))
.response("406", docs.not_acceptable_response())
.response("415", docs.unsupported_media_type_post_response())
.build();
let paths = PathsBuilder::new()
.path("/users", PathItem::new(HttpMethod::Post, create_user))
.build();
```
The generated `OpenAPI` operation includes every selected format media type:
```json
{
"/users": {
"post": {
"requestBody": {
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/CreateUserRequest" } },
"application/msgpack": { "schema": { "$ref": "#/components/schemas/CreateUserRequest" } },
"application/x-msgpack": { "schema": { "$ref": "#/components/schemas/CreateUserRequest" } }
},
"required": true
}
}
}
}
```
Use `request_body_with_content` and `response_with_content` when you
need Utoipa `Content` metadata such as examples, encodings, or extensions.
See `cargo run --example openapi --features "utoipa,json,msgpack"` for a runnable example.
## Design
The library uses an extractor/responder pattern where the negotiated format flows explicitly through handler signatures:
1. Middleware parses `Accept` and `Content-Type` headers
2. `Negotiate<T>` extractor deserializes the request body and captures the format
3. Handler calls `.respond(value)` to create a `NegotiateResponse<T>`
4. `IntoResponse` serializes using the captured format
This approach avoids heap allocation for response values and makes data flow visible in type signatures.
## License
MIT