tower-conneg 1.1.0

Tower middleware for HTTP content negotiation
docs.rs failed to build tower-conneg-1.1.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
  • 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)

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])
    .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
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:

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:

{
  "/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