File uploads
Take a file part straight from a multipart/form-data request with
poem’s Multipart extractor. #[routes] passes it through like any other
handler argument, so a direct upload is the single-round-trip alternative
to the presigned flow: the client posts the bytes, the server
stores them.
#[post("/uploads/direct")]async fn upload_direct(&self, mut form: Multipart) -> Result<Json<PresignedUrlDto>> { while let Some(field) = form.next_field().await.map_err(bad_request)? { if field.name() != Some("file") { continue; } let filename = field.file_name().map(str::to_owned).unwrap_or_default(); // Validate before the bytes become an object key. UploadRequestDto { filename: filename.clone() }.validate().map_err(unprocessable)?; let bytes = field.bytes().await.map_err(bad_request)?; let ticket = self.svc.store_upload(&filename, bytes).await.map_err(internal)?; return Ok(Json(ticket)); } Err(Error::from_string("multipart body has no `file` part", StatusCode::BAD_REQUEST))}Validate the filename at the edge
Section titled “Validate the filename at the edge”A multipart part’s filename is attacker-controlled — never let it become
an object key unchecked. Reuse the same validated DTO the rest of the
slice binds (UploadRequestDto), so one anti-traversal allowlist covers
both the presigned and the direct path. A rejected name is a 422, never
a stored object.
Presigned or direct
Section titled “Presigned or direct”| Presigned PUT | Direct multipart | |
|---|---|---|
| Bytes flow | client → storage | client → server → storage |
| Server memory | none | one part at a time |
| Round trips | two (mint, then PUT) | one |
| Use when | large media, offload the transfer | small files, one call |
Limits
Section titled “Limits”- The
max_body_bytescap gates theRawBodyandJsonextractors — notMultipart, which poem reads independently. A direct upload is bounded only by the field buffering below, so keep the admin gate and rate limit the demo binds, or prefer the presigned path. - The uploaded part is buffered whole per field before it reaches storage
(
field.bytes()); for very large files the presigned PUT avoids proxying through the server entirely.
Going further
Section titled “Going further”- File storage — the presigned flow and the object-store client.
audioin the demo — the direct upload in full.- Streaming responses — the download counterpart.