Body extractors
Read and deserialize request bodies — Json, Form, raw Bytes, Protobuf, SIMD JSON, and multipart uploads.
Body extractors
Body extractors consume the request body and deserialize it into a typed
handler argument. They implement
FromRequest,
so a handler may take at most one of them, and it must come after any
request-metadata extractors — the body is
read once. On a content-type mismatch or a deserialization failure the
extractor's error type becomes a 400 Bad Request response and the
handler is skipped.
Json<T>
The default JSON body extractor. T must implement serde::Deserialize.
Json<T> also implements Responder, so handlers can return it to emit a
JSON response with Content-Type: application/json.
use serde::{Deserialize, Serialize};
use tako::extractors::json::Json;
#[derive(Deserialize, Serialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(Json(user): Json<CreateUser>) -> Json<CreateUser> {
println!("creating {}", user.name);
Json(user)
}With the simd feature on, Json<T> automatically dispatches to a
SIMD-accelerated parser for large payloads; the threshold is configurable
per route via Route::simd_json(SimdJsonMode). For an unconditional SIMD
path, use SimdJson<T> below.
Form<T>
Parses an application/x-www-form-urlencoded body via serde_urlencoded.
T must implement serde::Deserialize. The extractor checks that the
Content-Type starts with application/x-www-form-urlencoded (the
; charset=utf-8 variant is accepted) and otherwise returns
FormError::InvalidContentType.
use serde::Deserialize;
use tako::extractors::form::Form;
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
async fn login(Form(form): Form<LoginForm>) {
println!("login attempt for {}", form.username);
}FormError covers InvalidContentType, BodyReadError, InvalidUtf8,
ParseError, and DeserializationError; all map to 400 Bad Request.
Bytes
Raw access to the underlying body stream, without buffering. Bytes<'a>
wraps &'a mut TakoBody, so you drive the read yourself (for streaming,
custom framing, or hashing). Its error type is Infallible.
use http_body_util::BodyExt;
use tako::extractors::bytes::Bytes;
async fn raw_body(Bytes(body): Bytes<'_>) {
let collected = body.collect().await.unwrap().to_bytes();
println!("read {} bytes", collected.len());
}This Bytes<'a> is a body reference, distinct from the refcounted
buffer bytes::Bytes from the bytes crate. In a handler that needs
both, import it aliased:
use tako::extractors::bytes::Bytes as BytesBody;.
Protobuf<T>
protobuf feature.Decodes a Protocol Buffers body into a prost::Message. The extractor
accepts application/x-protobuf or application/protobuf (with optional
parameters) and rejects everything else with
ProtobufError::InvalidContentType. Protobuf<T> also implements
Responder, encoding the message back out with
Content-Type: application/x-protobuf.
use prost::Message;
use tako::extractors::protobuf::Protobuf;
#[derive(Clone, PartialEq, Message)]
struct CreateUserRequest {
#[prost(string, tag = "1")]
pub name: String,
#[prost(string, tag = "2")]
pub email: String,
}
async fn create_user(Protobuf(req): Protobuf<CreateUserRequest>) -> String {
format!("creating {}", req.name)
}SimdJson<T>
simd feature.Forces SIMD-accelerated JSON parsing regardless of payload size, backed by
the simd_json crate. The sibling SonicJson<T> uses the sonic_rs
backend with the same API. Both validate the JSON content type, read the
full body, and implement Responder for SIMD-serialized JSON responses.
use serde::{Deserialize, Serialize};
use tako::extractors::simdjson::SimdJson;
#[derive(Deserialize, Serialize)]
struct User {
name: String,
email: String,
age: u32,
}
async fn create_user(SimdJson(user): SimdJson<User>) -> SimdJson<User> {
println!("creating {}", user.name);
SimdJson(user)
}SimdJsonError distinguishes InvalidContentType, MissingContentType,
BodyReadError, and DeserializationError, all mapping to
400 Bad Request.
Multipart
multipart feature.Two extractors handle multipart/form-data (file uploads and mixed
forms):
TakoMultipart<'a>— raw access. Wraps amulter::Multipart; you iterate fields manually withnext_field().TakoTypedMultipart<'a, T, F>— strongly typed. Deserializes every part intoT, using the file-field typeF(one ofUploadedFile,InMemoryFile, orBufferedUploadedFile) for parts that carry a filename.
use serde::Deserialize;
use tako::extractors::multipart::{TakoTypedMultipart, UploadedFile};
#[derive(Deserialize)]
struct FileUploadForm {
title: String,
description: String,
file: UploadedFile,
}
async fn upload(
TakoTypedMultipart { data: form, .. }: TakoTypedMultipart<'_, FileUploadForm, UploadedFile>,
) {
println!("uploaded {:?} ({} bytes)", form.file.file_name, form.file.size);
println!("saved to {:?}", form.file.path);
}UploadedFile streams to a temp file whose on-disk name is a fresh UUID —
the client-supplied filename is preserved only in UploadedFile.file_name
and never influences the path, which closes off path-traversal. The temp
file is removed on drop (RAII); call persist(dest) or disarm_cleanup()
to keep it.
Limits come from a MultipartConfig (inserted into request extensions or
set as global state): total_size_limit, per_part_size_limit (default
1 MiB), max_parts, allowed_content_types, disk_spill_threshold, and
field_chunk_timeout. The typed extractor enforces max_parts and the
content-type allow-list; the raw extractor leaves part-count enforcement
to the caller.
use anyhow::Result;
use http::Method;
use http::StatusCode;
use tako::extractors::FromRequest;
use tako::extractors::multipart::InMemoryFile;
use tako::extractors::multipart::TakoMultipart;
use tako::extractors::multipart::TakoTypedMultipart;
use tako::extractors::multipart::UploadedFile;
use tako::responder::Responder;
use tako::router::Router;
use tako::types::Request;
use tokio::net::TcpListener;
async fn upload_file(mut req: Request) -> impl Responder {
#[derive(serde::Deserialize)]
struct Form {
description: String,
file: UploadedFile,
}
let TakoTypedMultipart::<Form, UploadedFile> { data, .. } =
TakoTypedMultipart::from_request(&mut req).await.unwrap();
println!(
"uploaded {:?} ({} bytes) — description: {}",
data.file.file_name, data.file.size, data.description,
);
(StatusCode::OK, "File uploaded successfully")
}
async fn upload_mem(mut req: Request) -> impl Responder {
#[derive(serde::Deserialize)]
struct ImgForm {
title: String,
image: InMemoryFile,
}
let TakoTypedMultipart::<ImgForm, InMemoryFile> { data, .. } =
TakoTypedMultipart::from_request(&mut req).await.unwrap();
println!(
"image '{}' received ({} bytes in memory)",
data.title,
data.image.data.len(),
);
(StatusCode::OK, "Image uploaded successfully")
}
async fn raw_with_file(mut req: Request) -> impl Responder {
let TakoMultipart(mut mp) = TakoMultipart::from_request(&mut req).await.unwrap();
let mut total_files = 0usize;
while let Some(mut field) = mp.next_field().await.unwrap() {
if field.file_name().is_some() {
let fname = field
.file_name()
.map(|s| s.to_owned())
.unwrap_or_else(|| "<unnamed>".into());
total_files += 1;
let mut size = 0usize;
while let Some(chunk) = field.chunk().await.unwrap() {
size += chunk.len();
}
println!("received {fname} ({size} bytes)");
}
}
(StatusCode::OK, format!("processed {total_files} file(s)"))
}
async fn raw_text(mut req: Request) -> impl Responder {
use std::collections::HashMap;
use tako::types::BuildHasher;
let TakoMultipart(mut mp) = TakoMultipart::from_request(&mut req).await.unwrap();
let mut map: HashMap<String, String, BuildHasher> = HashMap::with_hasher(BuildHasher::default());
while let Some(field) = mp.next_field().await.unwrap() {
if field.file_name().is_some() {
return (StatusCode::BAD_REQUEST, "file not accepted");
}
let name = field.name().unwrap_or("noname").to_owned();
let text = field.text().await.unwrap();
map.insert(name, text);
}
(StatusCode::OK, "text form processed")
}
async fn typed_text(mut req: Request) -> impl Responder {
#[derive(serde::Deserialize)]
struct LoginForm {
username: String,
password: String,
}
let TakoTypedMultipart::<LoginForm, UploadedFile> { data, .. } =
TakoTypedMultipart::from_request(&mut req).await.unwrap();
println!(
"login attempt: username='{}' (password length: {})",
data.username,
data.password.len(),
);
(StatusCode::OK, "typed text processed")
}
#[tokio::main]
async fn main() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
let mut router = Router::new();
router.route(Method::POST, "/upload_file", upload_file);
router.route(Method::POST, "/upload_mem", upload_mem);
router.route(Method::POST, "/raw_with_file", raw_with_file);
router.route(Method::POST, "/raw_text", raw_text);
router.route(Method::POST, "/typed_text", typed_text);
tako::serve(listener, router).await;
Ok(())
}
Related
- Return-side encoding and the
Respondertrait — see Routing. - Bound body sizes per handler with
ContentLengthLimit<T, N>, or globally with the Body Limit middleware. - Non-body inputs — request metadata extractors.