🐙 tako
Extractors

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>

Requires the 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>

Requires the 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

Requires the multipart feature.

Two extractors handle multipart/form-data (file uploads and mixed forms):

  • TakoMultipart<'a> — raw access. Wraps a multer::Multipart; you iterate fields manually with next_field().
  • TakoTypedMultipart<'a, T, F> — strongly typed. Deserializes every part into T, using the file-field type F (one of UploadedFile, InMemoryFile, or BufferedUploadedFile) 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(())
}

On this page