tower-conneg 1.0.0

Tower middleware for HTTP content negotiation
docs.rs failed to build tower-conneg-1.0.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

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
  • 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)

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<()>:

async fn get_user(neg: Negotiate<()>, Path(id): Path<u64>) -> NegotiateResponse<User> {
    let user = db.get(id).await;
    neg.respond(user)
}

Quick Start (Client)

use tower_conneg::{ClientNegotiateLayer, ClientConfig, JsonFormat, MsgPackFormat};

let config = ClientConfig::builder()
    .formats([MsgPackFormat, JsonFormat])  // Priority order
    .fallback_format(JsonFormat)
    .build();

let client = ServiceBuilder::new()
    .layer(ClientNegotiateLayer::new(config))
    .service(hyper_client);

Available Formats

Format Feature Flag Media Type
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

Framework Feature Flag Notes
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

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