# Body extractors

> Read and deserialize request bodies — Json, Form, raw Bytes, Protobuf, SIMD JSON, and multipart uploads.

Body extractors consume the request body and deserialize it into a typed
handler argument. They implement
[`FromRequest`](https://docs.rs/tako-rs/latest/tako/extractors/trait.FromRequest.html),
so a handler may take **at most one** of them, and it must come after any
[request-metadata extractors](/docs/extractors/request-meta) — 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`.

```rust
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>`](#simdjsont) 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`.

```rust
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`, 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`.

```rust
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());
}
```

<Callout type="info">
  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;`.
</Callout>

## `Protobuf<T>`

<Callout type="info">
  Requires the 

  `protobuf`

   feature.
</Callout>

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`.

```rust
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>`

<Callout type="info">
  Requires the 

  `simd`

   feature.
</Callout>

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.

```rust
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`, `BodyReadError`, and
`DeserializationError`, all mapping to `400 Bad Request`.

## `Multipart`

<Callout type="info">
  Requires the 

  `multipart`

   feature.
</Callout>

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

* **`TakoMultipart<'a>`** — streaming access through `MultipartFields`; you
  iterate `multer::Field` values manually with `next_field()`. The stream
  keeps enforcing limits when accessed through `.0` or `into_inner()`.
* **`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.

```rust
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`, looked up in the request extensions,
then the router's state (`Router::with_state`), then the process-global store
(`get_or_init_state`): `total_size_limit`, `per_part_size_limit` (default
1 MiB), `max_parts`, `allowed_content_types`, `disk_spill_threshold`, and
`field_chunk_timeout`. Both extractors enforce size limits, `max_parts`
(413 when exceeded), and the content-type allow-list (415 when rejected).
The typed extractor also applies `field_chunk_timeout`; manual field
processing is responsible for its own read deadline.

Source: [examples/multipart/src/main.rs](https://github.com/rust-dd/tako/blob/main/examples/multipart/src/main.rs)

```rust
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::Server::builder()
    .build()
    .spawn_http(listener, router)
    .result()
    .await
    .expect("HTTP server failed");

  Ok(())
}
```

## Related

* Return-side encoding and the `Responder` trait — see [Routing](/docs/routing).
* Bound body sizes per handler with `ContentLengthLimit<T, N>`, or globally
  with the [Body Limit middleware](/docs/middleware).
* Non-body inputs — [request metadata extractors](/docs/extractors/request-meta).

Source: https://tako.rust-dd.com/docs/extractors/body