Manual contentsThe BasicsBrowse 113 chapters
Manual 21 min read

Requests

Suprnova handlers receive a Request - the wire-level HTTP request - or a typed form-request struct that parses, validates, and authorizes the body before your code runs. Both paths live on the same #[handler] macro; you pick the shape per route. This chapter covers both, plus the multipart upload extractor and the raw accessors you reach for in middleware.

Typed form requests

The #[request] attribute marks a struct as a FormRequest. The macro adds serde::Deserialize and validator::Validate derives and emits an impl FormRequest so the #[handler] macro knows to extract and validate it on the way in:

use suprnova::request;

#[request]
pub struct CreateUserRequest {
    #[validate(email(message = "Please provide a valid email address"))]
    pub email: String,

    #[validate(length(min = 8, message = "Password must be at least 8 characters"))]
    pub password: String,

    #[validate(length(min = 1, max = 100, message = "Name is required"))]
    pub name: String,
}

A handler that names this type as its parameter is handed an already-validated value:

use suprnova::{handler, json_response, Response};
use crate::requests::CreateUserRequest;

#[handler]
pub async fn store(form: CreateUserRequest) -> Response {
    // `form` is validated - this code only runs if every rule passed.
    json_response!({ "email": form.email, "name": form.name })
}

A handler that names Request instead gets the raw request through unchanged:

use suprnova::{handler, json_response, Request, Response};

#[handler]
pub async fn index(req: Request) -> Response {
    json_response!({ "path": req.path() })
}

Both are extractors - the #[handler] macro looks up FromRequest::from_request for every parameter type, and any struct that implements FormRequest gets a blanket FromRequest impl for free.

Validation rules

Validation runs through the validator crate. Common rules:

String validations

#[request]
pub struct ExampleRequest {
    // Required (non-empty)
    #[validate(length(min = 1, message = "This field is required"))]
    pub name: String,

    // Email format
    #[validate(email(message = "Invalid email address"))]
    pub email: String,

    // URL format
    #[validate(url(message = "Invalid URL"))]
    pub website: String,

    // Length constraints
    #[validate(length(min = 8, max = 100))]
    pub password: String,

    // Regex pattern - PHONE_REGEX must be a `static` or `const`
    // visible from the validator's expansion point. Declare it once,
    // typically in the same module:
    #[validate(regex(path = "PHONE_REGEX", message = "Invalid phone number"))]
    pub phone: String,
}

use std::sync::LazyLock;
use regex::Regex;

// validator 0.20 implements `AsRegex` for `std::sync::LazyLock<Regex>`
// but not for `once_cell::sync::Lazy<Regex>` - use the std type so the
// derive's `#[validate(regex(path = "..."))]` expansion typechecks.
static PHONE_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^\+?[0-9\s\-()]{7,20}$").unwrap());

Numeric validations

#[request]
pub struct ProductRequest {
    // Range validation - literals must match the field type. `f64`
    // takes `0.0` / `10000.0`, not the integer-literal `0` / `10000`.
    #[validate(range(min = 0.0, max = 10000.0, message = "Price must be between 0 and 10000"))]
    pub price: f64,

    // Minimum value
    #[validate(range(min = 1))]
    pub quantity: i32,

    // Maximum value
    #[validate(range(max = 100))]
    pub discount_percent: i32,
}

Nested and collection validations

use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct Address {
    #[validate(length(min = 1))]
    pub street: String,

    #[validate(length(min = 1))]
    pub city: String,
}

#[request]
pub struct OrderRequest {
    // Nested struct validation
    #[validate(nested)]
    pub shipping_address: Address,

    // Collection length
    #[validate(length(min = 1, message = "At least one item required"))]
    pub items: Vec<String>,
}

Common validation attributes

Attribute Description Example
email Valid email format #[validate(email)]
url Valid URL format #[validate(url)]
length String/collection length #[validate(length(min = 1, max = 100))]
range Numeric range #[validate(range(min = 0, max = 100))]
regex Regex pattern match #[validate(regex(path = "PATTERN"))]
contains String contains substring #[validate(contains(pattern = "@"))]
does_not_contain String doesn't contain #[validate(does_not_contain(pattern = "admin"))]
nested Validate nested struct #[validate(nested)]

Validation error responses

When validation fails, Suprnova returns a 422 response with the Laravel / Inertia-compatible error bag:

HTTP 422 Unprocessable Entity

{
    "message": "The given data was invalid.",
    "errors": {
        "email": ["Please provide a valid email address"],
        "password": ["Password must be at least 8 characters"]
    }
}

The errors shape matches what @inertiajs/* clients read from usePage().props.errors directly.

Nested fields

A #[validate(nested)] failure is reported under a dotted key naming the full path, the same notation Laravel uses. A nested struct contributes parent.field; an element of a validated Vec<T> contributes parent.<index>.field:

{
    "message": "The given data was invalid.",
    "errors": {
        "shipping_address.street": ["Validation failed for field 'shipping_address.street'"],
        "items.1.name": ["Validation failed for field 'items.1.name'"]
    }
}

Index 1 is the second element - the first element passed and is absent from the bag. Bind the key straight through on the client: form.errors['items.1.name'].

Complete example

A user registration endpoint, end to end.

Define the request:

// src/requests/create_user.rs
use suprnova::request;

#[request]
pub struct CreateUserRequest {
    #[validate(email(message = "Please provide a valid email address"))]
    pub email: String,

    #[validate(length(min = 8, message = "Password must be at least 8 characters"))]
    pub password: String,

    #[validate(length(min = 2, max = 50, message = "Name must be between 2 and 50 characters"))]
    pub name: String,
}

Create the controller:

// src/controllers/user.rs
use suprnova::{handler, json_response, Request, Response, ResponseExt};
use crate::requests::CreateUserRequest;

#[handler]
pub async fn index(_req: Request) -> Response {
    json_response!({ "users": [] })
}

#[handler]
pub async fn store(form: CreateUserRequest) -> Response {
    // Validation passed - create the user
    // In a real app, you'd save to database here

    json_response!({
        "user": {
            "email": form.email,
            "name": form.name
        },
        "message": "User created successfully"
    })
    .status(201)
}

Register the routes:

// src/routes.rs
use suprnova::{get, post, routes};
use crate::controllers;

routes! {
    get!("/users", controllers::user::index).name("users.index"),
    post!("/users", controllers::user::store).name("users.store"),
}

Authorization and cross-field hooks

The FormRequest trait exposes three lifecycle hooks: authorize, after_validation, and after_validation_async. Both the #[request] attribute and the #[derive(FormRequestDerive)] form emit a default impl FormRequest for you. To override any hook, add the #[form_request(custom_hooks)] opt-out to suppress the default impl, then write your own. (This mirrors the #[multipart(custom_hooks)] pattern.)

use suprnova::{FormRequest, FormRequestDerive, Request};
use serde::Deserialize;
use validator::Validate;

#[derive(Deserialize, Validate, FormRequestDerive)]
#[form_request(custom_hooks)]
pub struct DeleteUserRequest {
    pub user_id: i64,
}

impl FormRequest for DeleteUserRequest {
    fn authorize(req: &Request) -> bool {
        // Return false to short-circuit with 403 Forbidden before the
        // body is read.
        req.header("X-Admin-Token").is_some()
    }
}

The opt-out also works under the #[request] attribute form - useful when you want the attribute's auto-derives but need to override hooks:

use suprnova::{FormRequest, Request, request};

#[request]
#[form_request(custom_hooks)]
pub struct DeleteUserRequestAttr {
    pub user_id: i64,
}

impl FormRequest for DeleteUserRequestAttr {
    fn authorize(req: &Request) -> bool {
        req.header("X-Admin-Token").is_some()
    }
}

When authorize returns false, extraction returns FrameworkError::Unauthorized and renders:

HTTP 403 Forbidden

{ "message": "This action is unauthorized." }

after_validation is the synchronous cross-field hook - use it for rules like "password and confirmation must match". after_validation_async is the asynchronous counterpart and is where database-backed rules (e.g. the built-in Unique) participate in automatic validation. Both fire after the per-field validator rules pass; extract bails at the first failing stage.

use suprnova::{FormRequest, FormRequestDerive, ValidationErrors};
use serde::Deserialize;
use validator::Validate;

#[derive(Deserialize, Validate, FormRequestDerive)]
#[form_request(custom_hooks)]
pub struct UpdatePasswordRequest {
    #[validate(length(min = 8))]
    pub new_password: String,
    pub confirmation: String,
}

impl FormRequest for UpdatePasswordRequest {
    fn after_validation(&self) -> Result<(), ValidationErrors> {
        if self.new_password != self.confirmation {
            let mut errs = ValidationErrors::new();
            errs.add("confirmation", "passwords do not match");
            return Err(errs);
        }
        Ok(())
    }
}

Body size caps

The per-struct #[form_request(max_body_bytes = N)] attribute overrides the process-global 8 MiB cap on a single FormRequest:

use suprnova::FormRequestDerive;
use serde::Deserialize;
use validator::Validate;

#[derive(Deserialize, Validate, FormRequestDerive)]
#[form_request(max_body_bytes = 64 * 1024 * 1024)] // 64 MiB
pub struct ImportPayload {
    pub rows: Vec<Row>,
}

#[derive(Deserialize, Validate)]
pub struct Row { /* ... */ }

Content-Length is parsed up front and the request is rejected with HTTP 413 before a body byte is read when the declared size exceeds the cap; clients that lie about Content-Length still trip the streaming byte counter during read.

Content type detection

FormRequest::extract looks only at the Content-Type header:

  • application/x-www-form-urlencoded → parsed via serde_urlencoded
  • application/json or any application/*+json suffix → parsed via serde_json
  • Anything else (including a missing header) → rejected with HTTP 415 Unsupported Media Type, before the body is read

For multipart bodies (multipart/form-data), see file uploads below.

Reading the body directly

For one-off endpoints or middleware that doesn't want a full FormRequest, the Request type itself reads the body in three flavors - each consumes self because the body can be read at most once:

use serde::Deserialize;
use suprnova::{handler, json_response, Request, Response};

#[derive(Deserialize)]
struct LoginForm { username: String, password: String }

#[handler]
pub async fn login(req: Request) -> Response {
    // Pick the parser explicitly.
    let form: LoginForm = req.form().await?;
    json_response!({ "user": form.username })
}

#[handler]
pub async fn webhook(req: Request) -> Response {
    // Same shape, JSON wire.
    let payload: serde_json::Value = req.json().await?;
    json_response!({ "received": payload })
}

#[handler]
pub async fn ingest(req: Request) -> Response {
    // Auto-pick based on Content-Type - JSON unless
    // `application/x-www-form-urlencoded` is explicit.
    let value: serde_json::Value = req.input().await?;
    json_response!({ "value": value })
}

For raw access, req.body_bytes().await returns the buffered Bytes plus the RequestParts metadata (route params and content type). Use body_bytes_with_cap(n) to override the global 8 MiB cap on a case-by-case basis.

Resolving services alongside the form

Validated form requests compose with the service container. Use App::resolve::<T>() (or App::get::<T>()) inside the handler:

use suprnova::{handler, json_response, Response, App};
use crate::requests::CreateUserRequest;
use crate::services::UserService;

#[handler]
pub async fn store(form: CreateUserRequest) -> Response {
    let user_service = App::resolve::<UserService>()?;
    let user = user_service.create_user(&form.email, &form.name).await?;
    json_response!({ "user": user })
}

File uploads (MultipartRequest)

multipart/form-data is its own extractor - #[derive(MultipartRequest)] streams the body part by part, spilling large file parts to a temp file above the configured threshold so a 200 MiB upload never sits fully in RAM. Each field carries a #[field("name")] annotation that names the wire field; file fields use UploadedFile<V> where V is a validator (or a tuple of validators) from suprnova::http::upload::validators.

use suprnova::{handler, json_response, MultipartRequest, Response};
use suprnova::http::upload::UploadedFile;
use suprnova::http::upload::validators::{ImageFile, MaxSize};

#[derive(MultipartRequest)]
pub struct AvatarUpload {
    #[field("avatar")]
    pub avatar: UploadedFile<(ImageFile, MaxSize<5_242_880>)>, // 5 MiB cap
    #[field("caption")]
    pub caption: Option<String>,
}

#[handler]
pub async fn upload_avatar(form: AvatarUpload) -> Response {
    // `avatar` is in memory or in a temp file depending on size.
    // `.bytes()` reads either; `.store_as(...)` streams to a disk.
    let bytes = form.avatar.bytes().await?;
    json_response!({ "size": bytes.len(), "caption": form.caption })
}

Field shapes:

Declaration Wire shape
UploadedFile<V> required file
Option<UploadedFile<V>> optional file
Vec<UploadedFile<V>> array uploads (photos[])
String / u32 / any FromStr text field (required)
Option<String> / Option<T: FromStr> optional text field
Vec<String> / Vec<T: FromStr> repeated text fields

Built-in validators in suprnova::http::upload::validators:

  • MaxSize<N> - short-circuits at the byte boundary when the running total exceeds N bytes (HTTP 413).
  • ImageFile - rejects parts whose magic bytes don't claim image/*. (Named after Laravel's own rule; the plain Image name belongs to the image-manipulation pipeline - see Images.)
  • MimeType<L> - accepts a fixed allowlist provided by your own MimeAllowlist type.
  • () - no-op; UploadedFile<()> accepts any bytes.

Validators compose as tuples: (ImageFile, MaxSize<5_242_880>) runs both, short-circuiting on the first failure.

Per-field caps and array bounds

The byte cap on the total body is global (8 MiB by default for multipart, configurable via suprnova::http::upload::set_global_max_multipart_body_bytes). Per-field caps prevent abuse where a body of many small parts grows Vec<UploadedFile<_>> unbounded within the byte budget:

#[derive(MultipartRequest)]
pub struct Gallery {
    #[field("photos", max_count = 8)]
    pub photos: Vec<UploadedFile<MaxSize<1_048_576>>>,
}

The (max_count + 1)-th part with that name returns HTTP 422 before allocating, so the extra part never reaches Vec growth.

Authorize and after-validation hooks

MultipartRequest mirrors FormRequest's hooks via the MultipartRequestHooks trait. By default the derive emits an empty impl; opt in to your own with #[multipart(custom_hooks)]:

use suprnova::{MultipartRequest, Request, ValidationErrors};
use suprnova::http::upload::{MultipartRequestHooks, UploadedFile};

#[derive(MultipartRequest)]
#[multipart(custom_hooks)]
pub struct GuardedUpload {
    #[field("file")]
    pub file: UploadedFile,
}

impl MultipartRequestHooks for GuardedUpload {
    fn authorize(req: &Request) -> bool {
        req.header("X-Admin-Token").is_some()
    }

    fn after_validation(&self) -> Result<(), ValidationErrors> {
        if self.file.size == 0 {
            let mut errs = ValidationErrors::new();
            errs.add("file", "empty file");
            return Err(errs);
        }
        Ok(())
    }
}

Streaming to storage

UploadedFile::store_as writes the part to a registered storage disk. For disk-backed parts the path is fully streaming (64 KiB chunks via opendal::Operator::writer); in-memory parts use a single write call. Use the content-derived extension when the storage path is content-addressed - the filename header is untrusted:

use suprnova::Storage;

let disk = Storage::disk("avatars")?;
let path = format!("{}.{}", user.id, form.avatar.extension_from_magic());
form.avatar.store_as(&disk, &path).await?;

See Filesystem for the storage disk registry.

File organization

The standard structure for requests:

src/
├── requests/
│   ├── mod.rs                 # Re-exports all requests
│   ├── create_user.rs         # CreateUserRequest
│   ├── update_user.rs         # UpdateUserRequest
│   └── create_post.rs         # CreatePostRequest
├── controllers/
│   └── user.rs                # Uses CreateUserRequest
└── routes.rs

src/requests/mod.rs:

pub mod create_user;
pub mod update_user;

pub use create_user::CreateUserRequest;
pub use update_user::UpdateUserRequest;

End-to-end type safety with Inertia

Requests can also derive InertiaProps to generate TypeScript types, enabling end-to-end type safety from your Rust backend to your React frontend.

Generating TypeScript types for requests

Add InertiaProps derive alongside #[request]:

use suprnova::{request, InertiaProps};

#[request]
#[derive(InertiaProps)]
pub struct CreateTodoRequest {
    #[validate(length(min = 1, message = "Title is required"))]
    pub title: String,

    #[validate(length(max = 500))]
    pub description: Option<String>,
}

Run type generation:

suprnova generate-types

This generates TypeScript types in frontend/src/types/inertia-props.ts:

export interface CreateTodoRequest {
  title: string
  description: string | null
}

Type-safe forms with Inertia

Use Inertia's <Form> component for the cleanest form handling:

import { Form, usePage } from '@inertiajs/react'

export default function CreateTodo() {
  const { errors } = usePage().props

  return (
    <Form action="/todos" method="post">
      <input
        type="text"
        name="title"
        placeholder="Todo title"
      />
      {errors?.title && <span className="error">{errors.title}</span>}

      <textarea
        name="description"
        placeholder="Description (optional)"
      />

      <button type="submit">Create Todo</button>
    </Form>
  )
}

For more control, combine <Form> with the useForm hook and your generated types:

import { Form, useForm } from '@inertiajs/react'
import type { CreateTodoRequest } from '../types/inertia-props'

export default function CreateTodo() {
  const { data, setData, errors, processing } = useForm<CreateTodoRequest>({
    title: '',
    description: null,
  })

  return (
    <Form action="/todos" method="post">
      {({ processing }) => (
        <>
          <input
            type="text"
            name="title"
            value={data.title}
            onChange={(e) => setData('title', e.target.value)}
            placeholder="Todo title"
          />
          {errors.title && <span className="error">{errors.title}</span>}

          <textarea
            name="description"
            value={data.description || ''}
            onChange={(e) => setData('description', e.target.value || null)}
            placeholder="Description (optional)"
          />

          <button type="submit" disabled={processing}>
            Create Todo
          </button>
        </>
      )}
    </Form>
  )
}

What the derive buys you

  • TypeScript catches field-name typos and type mismatches at compile time.
  • IDE autocomplete reads the generated .ts directly.
  • Rename a field in Rust, rerun suprnova generate-types, and the TypeScript surface follows.

See TypeScript types for the full generation pipeline.

Request accessors

Beyond the validated-form pattern above, the Request type carries Laravel-style accessors for inspecting the wire-level request - URL, headers, query string, content negotiation, route metadata, and client IP. These are useful in middleware, in handlers that want raw access alongside a FormRequest, and in any place where validated parsing isn't the right tool.

URL and path

Method Returns Notes
req.path() &str Raw URI path.
req.decoded_path() String Path with percent-escapes resolved.
req.segments() Vec<String> Path split on /, empty segments dropped.
req.segment(index, default) Option<String> 1-based segment access.
req.url() String Scheme + host + path (no query string).
req.full_url() String URL + query string.
req.full_url_with_query(&[("k","v")]) String Append or override query keys.
req.full_url_without_query(&["k"]) String Strip query keys.
use suprnova::{handler, json_response, Request, Response};

#[handler]
pub async fn show(req: Request) -> Response {
    if req.is(&["admin/*"]) {
        // path matches the admin/* wildcard
    }
    json_response!({ "url": req.full_url() })
}

Host, scheme, IP

Method Returns Source order
req.host() Option<String> X-Forwarded-Host → Host → URI authority.
req.http_host() Option<String> Host plus port when non-default.
req.scheme_and_http_host() Option<String> scheme://host:port.
req.scheme() &'static str "https" when [secure] is true, else "http".
req.secure() bool URI scheme → X-Forwarded-Proto → X-Forwarded-Ssl: on.
req.ip() Option<String> From a trusted proxy: X-Forwarded-For, read from the right, then X-Real-IP when there is no X-Forwarded-For. Otherwise the peer address.
req.ips() Vec<String> Every address the request names: X-Forwarded-For left to right, X-Real-IP, then the peer address. For logs only.
req.user_agent() Option<&str> User-Agent header.
req.port() Option<u16> Host header port → X-Forwarded-Port → URI port.

req.ip() is the address to key a limit on or to check against an allowlist. It reads the proxy headers only when the TCP peer is a trusted proxy, and it returns the peer address in every other case:

  1. When the peer is not in the trusted proxies, req.ip() is the peer address. The headers are ignored, because any client can send them.
  2. When the peer is a trusted proxy and the request has an X-Forwarded-For header, req.ip() reads the list from the right. It skips every entry that is a trusted proxy and returns the first entry that is not. A proxy adds the address it saw to the right end of the header, so the left end is what the client wrote and the client chooses it. The header may arrive as several lines. They count as one list, in order.
  3. When the peer is a trusted proxy and the request has no X-Forwarded-For, req.ip() reads X-Real-IP. With both headers present, X-Real-IP is not read.
  4. An entry that is not an address ends the walk, and req.ip() returns the proxy that wrote that entry. An entry with a port, such as 203.0.113.5:54321, is read as its address.

Three rules keep this safe:

  • List every proxy of the chain. A proxy that is not in the trusted proxies is taken for the client, and all of its clients share its address.
  • List proxies and nothing else. APP_TRUSTED_PROXIES takes addresses and CIDR ranges, such as 10.0.0.5,173.245.48.0/20,2400:cb00::/32. A client that connects from a listed range is believed like a proxy and chooses its own address. A range of /0, which is every address, is refused, and boot fails.
  • Let the proxy write the header. A trusted proxy has to add to X-Forwarded-For or replace it. A proxy that passes the client's header on unchanged lets the client write all of it. A proxy that sets X-Real-IP has to remove any X-Forwarded-For from the request.

The Forwarded header of RFC 7239 is not read. See Rate Limiting for what this means for per-address limits and Environment Variables for the variable.

Headers and method

Method Returns
req.has_header("X-Foo") bool
req.bearer_token() Option<String> (last Bearer substring, comma-trimmed)
req.is_method("POST") bool (case-insensitive)
req.ajax() X-Requested-With: XMLHttpRequest
req.pjax() Truthy X-PJAX header
req.prefetch() X-Moz, Purpose, or Sec-Purpose = prefetch

Content negotiation

if req.is_json() { /* Content-Type carries /json or +json */ }
if req.expects_json() { /* AJAX without Accept narrowing, or Accept prefers JSON */ }
if req.wants_json() { /* Accept header tops with JSON */ }
if req.accepts_html() { /* Accept allows text/html */ }

let preferred = req.prefers(&["application/json", "text/html"]);
let acceptable = req.acceptable_content_types();

accepts(&[ty]) matches both bare types and application/<vendor>+json-style suffixes. accepts_any_content_type() returns true when there is no Accept header or the top preference is */*.

Query string

let id: Option<String> = req.query_param("id");
let present: bool = req.has_query("id");
let map = req.query_params(); // HashMap<String, String>

// Typed query parse via serde
#[derive(serde::Deserialize)]
struct SearchQuery { page: u32, q: String }
let q: SearchQuery = req.query_into()?;

Route metadata

After the router dispatches a request, the matched pattern is recorded on the request:

if req.route_is(&["users.show", "users.*"]) {
    // we're inside the users.show or users.* route
}

let pattern = req.route_pattern(); // Some("/users/{id}")
let name = req.route_name();       // Some("users.show")

route_is(&[...]) accepts * wildcards (Laravel's Str::is semantics).

Aborting early

For early-exit error handling without the full Response envelope, the abort_with / abort_if / abort_unless helpers return a FrameworkError that renders through the standard From<FrameworkError> for HttpResponse pipeline. They compose with ? directly:

use suprnova::{abort_if, abort_unless, abort_with, handler, json_response, Request, Response};

#[handler]
pub async fn show(req: Request) -> Response {
    let id = req.param("id")?;

    // 404 when the resource is missing.
    abort_if(id == "0", 404, "User not found")?;

    // 403 when the caller is unauthenticated.
    abort_unless(req.has_header("Authorization"), 403, "Login required")?;

    // Or raise a status unconditionally:
    if some_condition() {
        return Err(abort_with(418, "I'm a teapot").unwrap_err().into());
    }

    json_response!({ "id": id })
}

abort_if / abort_unless return Ok(()) when the condition is false, so the ? continues normally.

Why Suprnova diverges

Laravel exposes a synchronous, merged input bag - $req->input('field'), $req->all(), $req->only(['a','b']), $req->boolean('flag') - pulled from the query string and the parsed body together. Suprnova does not ship that surface. The reason:

  • Suprnova's body is consume-once and async. A synchronous all() would require buffering every body up front to satisfy a method that most handlers never call - the memory and DoS surface differs from PHP's per-request-process lifecycle.
  • The typed alternative (#[request] + FormRequest) gives compile-time field names, validation, and content-type-aware parsing - exactly the safety net the untyped bag lacks.

For query / header / route inspection, reach for query_param, query_into, has_query, bearer_token, and the header readers above. For body-side access, define a #[request] struct or a #[derive(MultipartRequest)] extractor.

Next

  • Validation - the rule library behind #[validate(...)] and the shape of the 422 error bag
  • Responses - building HttpResponse values back from your handler, including streaming and redirects
  • Errors - handler patterns built on top of Response being Result<HttpResponse, HttpResponse>
  • Routing - registering routes and the {id} parameters req.param("id") reads
  • Authentication - Auth::user_as, Auth::attempt, and the guards that resolve the current user from the request
  • Filesystem - registering the storage disks that UploadedFile::store_as writes to