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.
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
| Extractor | Description |
|---|---|
Json<T> | JSON body (with optional SIMD acceleration) |
Form<T> | URL-encoded form body |
Bytes | Raw request body stream |
Protobuf<T> | Protocol Buffers body (feature protobuf) |
SimdJson<T> | Force SIMD JSON parsing (feature simd) |
Multipart | Multipart form data (feature multipart) |
Request metadata — /docs/extractors/request-meta
| Extractor | Description |
|---|---|
Query<T> | URL query parameters |
Path<T> | Typed route path parameters |
Params<T> | Dynamic path params map |
HeaderMap | Full request headers |
Accept | Content negotiation |
AcceptLanguage | Language negotiation |
Range | HTTP Range header |
IpAddr | Client IP address |
Auth — /docs/extractors/auth
| Extractor | Description |
|---|---|
Basic | HTTP Basic credentials (BasicAuth) |
Bearer | Bearer token (BearerAuth) |
ApiKey | API key from header or query |
JwtClaimsUnverified<T> | JWT claims, signature not checked |
JwtClaimsVerified<T> | Signature-verified JWT claims |
Cookies — /docs/extractors/cookies
| Extractor | Description |
|---|---|
CookieJar | Cookie reading / writing |
CookieSigned | HMAC-signed cookies (SignedCookieJar) |
CookiePrivate | Encrypted cookies (PrivateCookieJar) |
State — /docs/state
| Extractor | Description |
|---|---|
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:
| Entry | Attaches | Read by |
|---|---|---|
Entries::CONN | ConnInfo and the peer SocketAddr | ConnectInfo<T>, IpAddr |
Entries::MATCHED_PATH | MatchedPath | MatchedPath |
Entries::PARAMS | the matched path parameters | Path<T>, Params<T>, TypedParams<T> |
Entries::STATE | router and route state | State<T>, IpAddr |
Entries::BODY_LIMIT | the body size limit | Json<T>, Form<T>, Bytes, String, Protobuf<T>, SimdJson<T> |
Entries::SIMD_JSON | the route's SIMD JSON mode | Json<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