Skip to content

Commit 11e56d2

Browse files
Document BlackSheep 2.6.0 multipart/form-data improvements (#32)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Roberto Prevato <roberto.prevato@gmail.com>
1 parent b81a400 commit 11e56d2

File tree

3 files changed

+407
-7
lines changed

3 files changed

+407
-7
lines changed

blacksheep/docs/binders.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This page describes:
1414

1515
- [X] Implicit and explicit bindings.
1616
- [X] Built-in binders.
17+
- [X] Multipart/form-data support (improved in 2.6.0).
1718
- [X] How to define a custom binder.
1819

1920
It is recommended to read the following pages before this one:
@@ -350,6 +351,137 @@ def home(accept: FromAcceptHeader, foo: FromFooCookie) -> Response:
350351
)
351352
```
352353

354+
## Multipart/Form-data Support
355+
356+
/// admonition | Significantly improved in BlackSheep 2.6.0
357+
type: info
358+
359+
BlackSheep 2.6.0 introduces major improvements for handling `multipart/form-data` requests with the `FromForm` and `FromFiles` binders:
360+
361+
- **Memory-efficient file handling**: Files use `SpooledTemporaryFile` - small files (<1MB) stay in memory, larger files automatically spill to temporary disk files
362+
- **True streaming parsing**: New streaming API for processing multipart data without buffering (`async for part in request.multipart_stream()`)
363+
- **Automatic resource cleanup**: The framework automatically cleans up file resources at the end of each request
364+
- **Better file API**: `FileBuffer` class provides clean methods (`read()`, `seek()`, `close()`, `save_to()`) for working with uploaded files
365+
- **OpenAPI documentation**: `FromText` and `FromFiles` are now fully documented in OpenAPI schemas
366+
367+
///
368+
369+
### FromForm: Form URL encoded and Multipart
370+
371+
The `FromForm` binder handles both `application/x-www-form-urlencoded` and `multipart/form-data` content types:
372+
373+
```python
374+
from dataclasses import dataclass
375+
from blacksheep import FromForm, post
376+
377+
378+
@dataclass
379+
class UserInput:
380+
name: str
381+
email: str
382+
age: int
383+
384+
385+
@post("/users")
386+
async def create_user(input: FromForm[UserInput]):
387+
user = input.value
388+
# user.name, user.email, user.age are automatically parsed
389+
return {"message": f"User {user.name} created"}
390+
```
391+
392+
### FromFiles: File Upload Handling
393+
394+
The `FromFiles` binder provides efficient handling of file uploads with memory-efficient buffering:
395+
396+
```python
397+
from blacksheep import FromFiles, post
398+
399+
400+
@post("/upload")
401+
async def upload_files(files: FromFiles):
402+
# files.value is a list of FormPart objects
403+
for file_part in files.value:
404+
# Access file metadata
405+
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
406+
content_type = file_part.content_type.decode() if file_part.content_type else None
407+
408+
# file_part.file is a FormPart instance with efficient memory handling
409+
# Small files (<1MB) are kept in memory, larger files use temporary disk files
410+
file_buffer = file_part.file
411+
412+
# Or save directly to disk (recommended for large files)
413+
await file_buffer.save_to(f"./uploads/{file_name}")
414+
415+
return {"uploaded": len(files.value)}
416+
```
417+
418+
### Mixed Multipart Forms: Files and Text Together
419+
420+
Since version 2.6.0, you can use `FromText` and `FromFiles` together to handle forms with both text fields and file uploads:
421+
422+
```python
423+
from blacksheep import FromFiles, FromText, post
424+
425+
426+
@post("/upload-with-description")
427+
async def upload_with_metadata(
428+
description: FromText,
429+
files: FromFiles,
430+
):
431+
# description.value contains the text field value
432+
text_content = description.value
433+
434+
# files.value contains the uploaded files
435+
for file_part in files.value:
436+
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
437+
438+
# Process the file
439+
await file_part.file.save_to(f"./uploads/{file_name}")
440+
441+
return {
442+
"description": text_content,
443+
"files_count": len(files.value)
444+
}
445+
```
446+
447+
### Memory-Efficient Streaming for Large Files
448+
449+
For handling very large file uploads efficiently, use the streaming API directly:
450+
451+
```python
452+
from blacksheep import post, Request, created
453+
454+
455+
@post("/upload-large")
456+
async def upload_large_files(request: Request):
457+
# Stream multipart data without buffering
458+
async for part in request.multipart_stream():
459+
if part.file_name:
460+
# This is a file upload
461+
file_name = part.file_name.decode()
462+
463+
# Stream the file content in chunks
464+
with open(f"./uploads/{file_name}", "wb") as f:
465+
async for chunk in part.stream():
466+
f.write(chunk)
467+
else:
468+
# This is a regular form field
469+
field_name = part.name.decode() if part.name else ""
470+
field_value = part.data.decode()
471+
472+
return created()
473+
```
474+
475+
### Automatic Resource Cleanup
476+
477+
BlackSheep automatically manages file resources. The framework calls `Request.dispose()` at the end of each request-response cycle to clean up temporary files created during multipart parsing. This ensures:
478+
479+
- Temporary files are automatically deleted
480+
- Memory is properly released
481+
- No manual cleanup is required in most cases
482+
483+
If you need manual control over resource cleanup (e.g., in long-running background tasks), you can call `request.dispose()` explicitly.
484+
353485
## Defining a custom binder
354486

355487
To define a custom binder, define a `BoundValue[T]` class and a `Binder`

blacksheep/docs/openapi.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,130 @@ components: {}
222222
tags: []
223223
```
224224
225+
### Request body binders support
226+
227+
/// admonition | Enhanced in BlackSheep 2.6.0
228+
type: info
229+
230+
BlackSheep 2.6.0 adds full OpenAPI documentation support for `FromText` and `FromFiles` binders. These binders are now automatically documented with appropriate request body schemas and content types.
231+
232+
///
233+
234+
BlackSheep automatically generates OpenAPI documentation for various request body binders. The following examples assume the `docs` handler has been set up as described in the [Built-in support](#built-in-support-for-openapi-documentation) section.
235+
236+
#### FromJSON
237+
238+
Documented with `application/json` content type and the appropriate schema:
239+
240+
```python
241+
from dataclasses import dataclass
242+
from blacksheep import FromJSON, post
243+
244+
245+
@dataclass
246+
class CreateUserInput:
247+
name: str
248+
email: str
249+
age: int
250+
251+
252+
@docs(
253+
summary="Create a new user",
254+
responses={201: "User created successfully"}
255+
)
256+
@post("/api/users")
257+
async def create_user(input: FromJSON[CreateUserInput]):
258+
return {"user_id": 123}
259+
```
260+
261+
The OpenAPI documentation automatically includes the request body schema for `CreateUserInput`.
262+
263+
#### FromFiles (since 2.6.0)
264+
265+
Documented with `multipart/form-data` content type:
266+
267+
```python
268+
from blacksheep import FromFiles, post
269+
270+
271+
@docs(
272+
summary="Upload files",
273+
responses={201: "Files uploaded successfully"}
274+
)
275+
@post("/api/upload")
276+
async def upload_files(files: FromFiles):
277+
return {"files_count": len(files.value)}
278+
```
279+
280+
The OpenAPI documentation automatically documents this as a file upload endpoint with `multipart/form-data` encoding.
281+
282+
#### FromText (since 2.6.0)
283+
284+
Documented with `text/plain` content type:
285+
286+
```python
287+
from blacksheep import FromText, post
288+
289+
290+
@docs(
291+
summary="Store text content",
292+
responses={201: "Text stored successfully"}
293+
)
294+
@post("/api/text")
295+
async def store_text(content: FromText):
296+
return {"length": len(content.value)}
297+
```
298+
299+
#### Mixed multipart/form-data (since 2.6.0)
300+
301+
When combining `FromText` and `FromFiles` in the same endpoint, BlackSheep generates appropriate `multipart/form-data` documentation:
302+
303+
```python
304+
from blacksheep import FromFiles, FromText, post
305+
306+
307+
@docs(
308+
summary="Upload files with description",
309+
responses={201: "Upload completed successfully"}
310+
)
311+
@post("/api/upload-with-metadata")
312+
async def upload_with_metadata(
313+
description: FromText,
314+
files: FromFiles,
315+
):
316+
return {
317+
"description": description.value,
318+
"files_count": len(files.value)
319+
}
320+
```
321+
322+
The OpenAPI specification will correctly document both the text field and file upload field as part of the `multipart/form-data` request body.
323+
324+
#### FromForm
325+
326+
Documented with `application/x-www-form-urlencoded` or `multipart/form-data` content type:
327+
328+
```python
329+
from dataclasses import dataclass
330+
from blacksheep import FromForm, post
331+
332+
333+
@dataclass
334+
class ContactForm:
335+
name: str
336+
email: str
337+
message: str
338+
339+
340+
@docs(
341+
summary="Submit contact form",
342+
responses={200: "Form submitted successfully"}
343+
)
344+
@post("/api/contact")
345+
async def submit_contact(form: FromForm[ContactForm]):
346+
return {"status": "received"}
347+
```
348+
225349
### Adding description and summary
226350

227351
An endpoint description can be specified either using a `docstring`:

0 commit comments

Comments
 (0)