🐙 tako
Extractors

Extractors

Typed handler arguments that read shape out of a request — JSON, form, query, path, headers, cookies, JWT, and more.

Extractors read shape out of a request and surface it as typed handler arguments. Any type implementing FromRequest (consumes the body) or FromRequestParts (headers / URL only) can be used as a handler parameter. A handler may take any number of extractors; Tako runs them in order before calling the handler.

use serde::{Deserialize, Serialize};
use tako::Method;
use tako::extractors::json::Json;
use tako::extractors::path::Path;
use tako::extractors::query::Query;
use tako::extractors::state::State;
use tako::responder::Responder;
use tako::router::Router;

#[derive(Deserialize)]
struct ListQuery { page: u32, per_page: u32 }

#[derive(Deserialize)]
struct UserPath { id: u64 }

#[derive(Deserialize, Serialize)]
struct CreatePost { title: String, body: String }

#[derive(Clone)]
struct Db; // imagine a real pool here

async fn list_posts(
  Path(UserPath { id }): Path<UserPath>,
  Query(q): Query<ListQuery>,
  State(_db): State<Db>,
) -> impl Responder {
  format!("user={id}, page={}, per_page={}", q.page, q.per_page)
}

async fn create_post(
  Path(UserPath { id }): Path<UserPath>,
  Json(p): Json<CreatePost>,
) -> Json<CreatePost> {
  println!("creating post for user={id}: {:?}", p.title);
  Json(p)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;

  let mut router = Router::new();
  router.with_state(Db);
  router.route(Method::GET,  "/users/{id}/posts", list_posts);
  router.route(Method::POST, "/users/{id}/posts", create_post);

  tako::Server::builder().build().spawn_http(listener, router)
    .result().await.expect("HTTP server failed");
  Ok(())
}

How binding works

Each extractor is a wrapper type you destructure in the argument position — Json(payload): Json<T>, Path(p): Path<T>, and so on. The body extractors (FromRequest) consume the request body and must come last in the argument list, because the body can only be read once. The parts extractors (FromRequestParts) only touch headers and the URI, so any number of them can run before the body extractor.

If extraction fails — a malformed JSON body, a missing required query parameter, a bad Authorization header — the extractor's error type is turned into a Responder (usually a 400 or 401) and the handler is never called. Route-level error handlers and use_problem_json() can reshape those errors before they hit the wire.

examples/extractors-multi/src/main.rs
use anyhow::Result;
use serde::Deserialize;
use serde::Serialize;
use tako::Method;
use tako::extractors::json::Json;
use tako::extractors::params::Params;
use tako::extractors::query::Query;
use tako::router::Router;
use tokio::net::TcpListener;

#[derive(Deserialize)]
struct Pagination {
  page: u32,
  per_page: u32,
}

#[derive(Deserialize)]
struct UserPath {
  id: u64,
}

#[derive(Deserialize, Serialize, Clone)]
struct CreateUser {
  name: String,
  email: String,
}

#[derive(Serialize)]
struct Created {
  id: u64,
  name: String,
  email: String,
}

// GET /users/{id}/posts?per_page=10&page=2
// Demonstrates multiple extractors: Params + Query
async fn list_user_posts(Params(user): Params<UserPath>, Query(p): Query<Pagination>) -> String {
  format!(
    "user_id={}, page={}, per_page={}",
    user.id, p.page, p.per_page
  )
}

// POST /users with JSON body {"name":"...","email":"..."}
// Demonstrates a body extractor
async fn create(Json(user): Json<CreateUser>) -> Json<Created> {
  // Normally you'd persist the user; here we just echo back with an id
  Json(Created {
    id: 1,
    name: user.name,
    email: user.email,
  })
}

#[tokio::main]
async fn main() -> Result<()> {
  let listener = TcpListener::bind("127.0.0.1:8080").await?;

  let mut router = Router::new();
  router.route(Method::GET, "/users/{id}/posts", list_user_posts);
  router.route(Method::POST, "/users", create);

  tako::Server::builder()
    .build()
    .spawn_http(listener, router)
    .result()
    .await
    .expect("HTTP server failed");

  Ok(())
}

Catalog

Tako ships 22+ bundled extractors. They fall into five groups, each with its own reference page.

Body — /docs/extractors/body

ExtractorDescription
Json<T>JSON body (with optional SIMD acceleration)
Form<T>URL-encoded form body
BytesRaw request body stream
Protobuf<T>Protocol Buffers body (feature protobuf)
SimdJson<T>Force SIMD JSON parsing (feature simd)
MultipartMultipart form data (feature multipart)

Request metadata — /docs/extractors/request-meta

ExtractorDescription
Query<T>URL query parameters
Path<T>Typed route path parameters
Params<T>Dynamic path params map
HeaderMapFull request headers
AcceptContent negotiation
AcceptLanguageLanguage negotiation
RangeHTTP Range header
IpAddrClient IP address

Auth — /docs/extractors/auth

ExtractorDescription
BasicHTTP Basic credentials (BasicAuth)
BearerBearer token (BearerAuth)
ApiKeyAPI key from header or query
JwtClaimsUnverified<T>JWT claims, signature not checked
JwtClaimsVerified<T>Signature-verified JWT claims

Cookies — /docs/extractors/cookies

ExtractorDescription
CookieJarCookie reading / writing
CookieSignedHMAC-signed cookies (SignedCookieJar)
CookiePrivateEncrypted cookies (PrivateCookieJar)

State — /docs/state

ExtractorDescription
State<T>Shared application state, as Arc<T>

Beyond the catalog

The crate also bundles QueryMulti<T> (repeated keys), RawPath / RawQuery, MatchedPath, OriginalUri, Host, Scheme, TypedHeader<H>, Extension<T>, ConnectInfo<T>, ContentLengthLimit<T, N>, and Validated<T> (behind the validator / garde features). The zero-copy-extractors feature enables borrowing variants of the body and header extractors for hot-path handlers. See the crate rustdoc for the complete list.

Writing an extractor

The last handler argument must implement FromRequest; earlier ones must implement FromRequestParts. An extractor that only reads headers, the URI, or extensions implements both, so it fits any position. Set ENTRIES to the framework entries it reads; this one reads none:

use http::request::Parts;
use tako::StatusCode;
use tako::extractors::Entries;
use tako::extractors::FromRequest;
use tako::extractors::FromRequestParts;
use tako::header::HeaderMap;
use tako::types::Request;

struct RequestId(String);

fn request_id(headers: &HeaderMap) -> Result<RequestId, StatusCode> {
  headers
    .get("x-request-id")
    .and_then(|value| value.to_str().ok())
    .map(|id| RequestId(id.to_owned()))
    .ok_or(StatusCode::BAD_REQUEST)
}

impl<'a> FromRequestParts<'a> for RequestId {
  type Error = StatusCode;
  const ENTRIES: Entries = Entries::NONE;

  async fn from_request_parts(parts: &'a mut Parts) -> Result<Self, Self::Error> {
    request_id(&parts.headers)
  }
}

impl<'a> FromRequest<'a> for RequestId {
  type Error = StatusCode;
  const ENTRIES: Entries = Entries::NONE;

  async fn from_request(req: &'a mut Request) -> Result<Self, Self::Error> {
    request_id(req.headers())
  }
}

The router can attach these entries to a request's extensions:

EntryAttachesRead by
Entries::CONNConnInfo and the peer SocketAddrConnectInfo<T>, IpAddr
Entries::MATCHED_PATHMatchedPathMatchedPath
Entries::PARAMSthe matched path parametersPath<T>, Params<T>, TypedParams<T>
Entries::STATErouter and route stateState<T>, IpAddr
Entries::BODY_LIMITthe body size limitJson<T>, Form<T>, Bytes, String, Protobuf<T>, SimdJson<T>
Entries::SIMD_JSONthe route's SIMD JSON modeJson<T>

A route without middleware or other per-request hooks gets only the entries its handler's extractors list, which saves work on every request; all other requests get every entry. Combine entries with Entries::CONN.union(Entries::STATE). ENTRIES defaults to Entries::ALL, which is always correct. An extractor that reads an entry it does not list finds it missing on those routes.

Last updated on

On this page