From 598669b7150d73342a4bf7bdeb9c0582d748ab0b Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:10 +0700 Subject: [PATCH 01/20] chore: update mkdocs.yml --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index d076451..b30fa34 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,6 +82,7 @@ nav: - en/tutorial/first-steps.md - en/tutorial/http-methods.md - en/tutorial/request-parameters.md + - en/tutorial/file-uploads.md - en/tutorial/response-handling.md - en/tutorial/parallel-execution.md - en/tutorial/async-session.md @@ -147,6 +148,7 @@ nav: - ru/tutorial/first-steps.md - ru/tutorial/http-methods.md - ru/tutorial/request-parameters.md + - ru/tutorial/file-uploads.md - ru/tutorial/response-handling.md - ru/tutorial/parallel-execution.md - ru/tutorial/async-session.md From eecfc534e5e115fa738273abf58581c5c1b4f8d1 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:12 +0700 Subject: [PATCH 02/20] docs: create file-uploads.md --- docs/en/tutorial/file-uploads.md | 126 +++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/en/tutorial/file-uploads.md diff --git a/docs/en/tutorial/file-uploads.md b/docs/en/tutorial/file-uploads.md new file mode 100644 index 0000000..5cf2080 --- /dev/null +++ b/docs/en/tutorial/file-uploads.md @@ -0,0 +1,126 @@ +# File Uploads + +FastHTTP supports multipart file uploads via the `files` parameter. + +## Simple Upload + +Pass bytes directly: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://api.example.com/upload", + files={"file": b"Hello, world!"}, +) +async def upload(resp: Response) -> dict: + return resp.json() +``` + +## Upload with Filename and Content Type + +Provide a tuple of `(filename, data, content_type)`: + +```python +@app.post( + url="https://api.example.com/upload", + files={"file": ("report.csv", b"name,score\nAlice,95\n", "text/csv")}, +) +async def upload_csv(resp: Response) -> dict: + return resp.json() +``` + +## Multiple Files + +Pass a dictionary with multiple keys: + +```python +@app.post( + url="https://api.example.com/upload", + files={ + "avatar": ("photo.jpg", b"\xff\xd8\xff\xe0...", "image/jpeg"), + "document": ("resume.pdf", b"%PDF-1.4...", "application/pdf"), + }, +) +async def upload_multi(resp: Response) -> dict: + return resp.json() +``` + +Or use a list to send multiple files under the same field name: + +```python +@app.post( + url="https://api.example.com/upload", + files=[ + ("files", ("a.txt", b"content of a", "text/plain")), + ("files", ("b.txt", b"content of b", "text/plain")), + ], +) +async def upload_list(resp: Response) -> dict: + return resp.json() +``` + +## Upload with JSON + +Combine `files` with `json` to send metadata alongside the file: + +```python +@app.post( + url="https://api.example.com/upload", + json={"title": "My photo", "tags": ["nature"]}, + files={"file": ("sunset.jpg", b"\xff\xd8\xff\xe0...", "image/jpeg")}, +) +async def upload_with_meta(resp: Response) -> dict: + return resp.json() +``` + +## File Object + +Pass an open file handle: + +```python +@app.post( + url="https://api.example.com/upload", + files={"file": open("photo.jpg", "rb")}, +) +async def upload_file(resp: Response) -> dict: + return resp.json() +``` + +## Path Object + +Pass a `pathlib.Path`: + +```python +from pathlib import Path + +FILE = Path("photo.jpg") + + +@app.post( + url="https://api.example.com/upload", + files={"file": FILE}, +) +async def upload_path(resp: Response) -> dict: + return resp.json() +``` + +## Supported Types + +The `files` parameter accepts: + +| Type | Example | +|------|---------| +| `bytes` | `b"raw data"` | +| `str` | `"text content"` | +| file object | `open("file.txt", "rb")` | +| `Path` | `Path("file.txt")` | +| tuple `(name, data)` | `("file.txt", b"data")` | +| tuple `(name, data, type)` | `("file.txt", b"data", "text/plain")` | +| dict `name -> data` | `{"file": b"data"}` | +| dict `name -> tuple` | `{"file": ("f.txt", b"data")}` | +| list of tuples | `[("f", b"a"), ("f", b"b")]` | From 39e4cb032852cd5544a63f0756d6f92b0180beff Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:15 +0700 Subject: [PATCH 03/20] docs: update http-methods.md --- docs/en/tutorial/http-methods.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/tutorial/http-methods.md b/docs/en/tutorial/http-methods.md index 60b2192..a6e5b15 100644 --- a/docs/en/tutorial/http-methods.md +++ b/docs/en/tutorial/http-methods.md @@ -85,6 +85,7 @@ async def allowed_methods(resp: Response) -> dict: | `params` | Query parameters | | `json` | JSON body (for POST, PUT, PATCH) | | `data` | Raw bytes body | +| `files` | File uploads (multipart/form-data) | | `tags` | Tags for grouping | | `dependencies` | List of dependencies | | `response_model` | Pydantic model for validation | From 53733bb52d8c54ae58945bb42ad5ded426111b15 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:17 +0700 Subject: [PATCH 04/20] docs: update request-parameters.md --- docs/en/tutorial/request-parameters.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/en/tutorial/request-parameters.md b/docs/en/tutorial/request-parameters.md index 0fade7d..aec48af 100644 --- a/docs/en/tutorial/request-parameters.md +++ b/docs/en/tutorial/request-parameters.md @@ -85,6 +85,21 @@ async def login(resp: Response) -> dict: return resp.json() ``` +## File Uploads + +Use `files` to upload files as multipart/form-data: + +```python +@app.post( + url="https://api.example.com/upload", + files={"file": open("photo.jpg", "rb")}, +) +async def upload(resp: Response) -> dict: + return resp.json() +``` + +See the full [File Uploads](file-uploads.md) guide for more examples. + ## Combining Parameters You can combine multiple parameters: From e14c6c8fe7544de70f189516173e66fb24fb240c Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:19 +0700 Subject: [PATCH 05/20] docs: create file-uploads.md --- docs/ru/tutorial/file-uploads.md | 126 +++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/ru/tutorial/file-uploads.md diff --git a/docs/ru/tutorial/file-uploads.md b/docs/ru/tutorial/file-uploads.md new file mode 100644 index 0000000..c1ca9a8 --- /dev/null +++ b/docs/ru/tutorial/file-uploads.md @@ -0,0 +1,126 @@ +# Загрузка файлов + +FastHTTP поддерживает загрузку файлов через multipart/form-data с помощью параметра `files`. + +## Простая загрузка + +Передайте байты напрямую: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://api.example.com/upload", + files={"file": b"Hello, world!"}, +) +async def upload(resp: Response) -> dict: + return resp.json() +``` + +## Загрузка с именем файла и типом контента + +Передайте кортеж `(имя_файла, данные, тип_контента)`: + +```python +@app.post( + url="https://api.example.com/upload", + files={"file": ("report.csv", b"name,score\nAlice,95\n", "text/csv")}, +) +async def upload_csv(resp: Response) -> dict: + return resp.json() +``` + +## Несколько файлов + +Передайте словарь с несколькими ключами: + +```python +@app.post( + url="https://api.example.com/upload", + files={ + "avatar": ("photo.jpg", b"\xff\xd8\xff\xe0...", "image/jpeg"), + "document": ("resume.pdf", b"%PDF-1.4...", "application/pdf"), + }, +) +async def upload_multi(resp: Response) -> dict: + return resp.json() +``` + +Или используйте список для отправки нескольких файлов под одним именем поля: + +```python +@app.post( + url="https://api.example.com/upload", + files=[ + ("files", ("a.txt", b"содержимое a", "text/plain")), + ("files", ("b.txt", b"содержимое b", "text/plain")), + ], +) +async def upload_list(resp: Response) -> dict: + return resp.json() +``` + +## Загрузка с JSON + +Сочетайте `files` с `json` для отправки метаданных вместе с файлом: + +```python +@app.post( + url="https://api.example.com/upload", + json={"title": "Моё фото", "tags": ["природа"]}, + files={"file": ("sunset.jpg", b"\xff\xd8\xff\xe0...", "image/jpeg")}, +) +async def upload_with_meta(resp: Response) -> dict: + return resp.json() +``` + +## Файловый объект + +Передайте открытый файловый дескриптор: + +```python +@app.post( + url="https://api.example.com/upload", + files={"file": open("photo.jpg", "rb")}, +) +async def upload_file(resp: Response) -> dict: + return resp.json() +``` + +## Path объект + +Передайте `pathlib.Path`: + +```python +from pathlib import Path + +FILE = Path("photo.jpg") + + +@app.post( + url="https://api.example.com/upload", + files={"file": FILE}, +) +async def upload_path(resp: Response) -> dict: + return resp.json() +``` + +## Поддерживаемые типы + +Параметр `files` принимает: + +| Тип | Пример | +|-----|--------| +| `bytes` | `b"данные"` | +| `str` | `"текст"` | +| файловый объект | `open("file.txt", "rb")` | +| `Path` | `Path("file.txt")` | +| кортеж `(имя, данные)` | `("file.txt", b"data")` | +| кортеж `(имя, данные, тип)` | `("file.txt", b"data", "text/plain")` | +| словарь `имя -> данные` | `{"file": b"data"}` | +| словарь `имя -> кортеж` | `{"file": ("f.txt", b"data")}` | +| список кортежей | `[("f", b"a"), ("f", b"b")]` | From a68c1ded505ae6212db452142cfb6efea7b09a7f Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:21 +0700 Subject: [PATCH 06/20] docs: update http-methods.md --- docs/ru/tutorial/http-methods.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/ru/tutorial/http-methods.md b/docs/ru/tutorial/http-methods.md index 002abbc..49ffb8c 100644 --- a/docs/ru/tutorial/http-methods.md +++ b/docs/ru/tutorial/http-methods.md @@ -77,6 +77,21 @@ async def allowed_methods(resp: Response) -> dict: return {"allow": resp.headers.get("allow", "")} ``` +## Параметры декоратора + +| Параметр | Описание | +|----------|----------| +| `url` | URL запроса (обязательный) | +| `params` | Query параметры | +| `json` | JSON тело (для POST, PUT, PATCH) | +| `data` | Сырые байты | +| `files` | Загрузка файлов (multipart/form-data) | +| `tags` | Теги для группировки | +| `dependencies` | Список зависимостей | +| `response_model` | Модель Pydantic для валидации | +| `request_model` | Модель Pydantic для валидации запроса | +| `responses` | Модели Pydantic для ответов с ошибками | + ## Возвращаемые значения Обработчики могут возвращать разные типы: From 427d8b7daf81c1e20ced510e9e565120a5ebe981 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:24 +0700 Subject: [PATCH 07/20] docs: update request-parameters.md --- docs/ru/tutorial/request-parameters.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/ru/tutorial/request-parameters.md b/docs/ru/tutorial/request-parameters.md index 9adefa4..771f190 100644 --- a/docs/ru/tutorial/request-parameters.md +++ b/docs/ru/tutorial/request-parameters.md @@ -84,3 +84,18 @@ async def with_headers(resp: Response) -> dict: async def slow_request(resp: Response) -> dict: return resp.json() ``` + +## Загрузка файлов + +Используйте `files` для загрузки файлов как multipart/form-data: + +```python +@app.post( + url="https://api.example.com/upload", + files={"file": open("photo.jpg", "rb")}, +) +async def upload(resp: Response) -> dict: + return resp.json() +``` + +Подробное руководство см. в разделе [Загрузка файлов](file-uploads.md). From 42b6259ab3d9b5e2b09d97f820c1bac87bd6d68b Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:27 +0700 Subject: [PATCH 08/20] feat: create examples/files/__init__.py --- examples/files/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/files/__init__.py diff --git a/examples/files/__init__.py b/examples/files/__init__.py new file mode 100644 index 0000000..e69de29 From ccee49ba2debbad6e869827b495dbe32b6515464 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:30 +0700 Subject: [PATCH 09/20] feat: create simple_upload.py --- examples/files/simple_upload.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/files/simple_upload.py diff --git a/examples/files/simple_upload.py b/examples/files/simple_upload.py new file mode 100644 index 0000000..ba0cecd --- /dev/null +++ b/examples/files/simple_upload.py @@ -0,0 +1,16 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://httpbin.org/post", + files={"file": b"Hello, world!"}, +) +async def upload_bytes(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() From 6d3b54435f77d14d8c09d77d8a8c30a27801234f Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:33 +0700 Subject: [PATCH 10/20] feat: create upload_file_object.py --- examples/files/upload_file_object.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/files/upload_file_object.py diff --git a/examples/files/upload_file_object.py b/examples/files/upload_file_object.py new file mode 100644 index 0000000..b974023 --- /dev/null +++ b/examples/files/upload_file_object.py @@ -0,0 +1,16 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://httpbin.org/post", + files={"file": open(__file__, "rb")}, +) +async def upload_open_file(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() From de67a109c2188cb12dc11c72396bdfcf97b59e24 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:35 +0700 Subject: [PATCH 11/20] feat: create upload_list.py --- examples/files/upload_list.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/files/upload_list.py diff --git a/examples/files/upload_list.py b/examples/files/upload_list.py new file mode 100644 index 0000000..47ddc49 --- /dev/null +++ b/examples/files/upload_list.py @@ -0,0 +1,19 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://httpbin.org/post", + files=[ + ("files", ("a.txt", b"content of a", "text/plain")), + ("files", ("b.txt", b"content of b", "text/plain")), + ], +) +async def upload_list(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() From 127ed28b6738fcfca91d74731e41b1c723235013 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:37 +0700 Subject: [PATCH 12/20] feat: create upload_multiple.py --- examples/files/upload_multiple.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/files/upload_multiple.py diff --git a/examples/files/upload_multiple.py b/examples/files/upload_multiple.py new file mode 100644 index 0000000..cdb17b5 --- /dev/null +++ b/examples/files/upload_multiple.py @@ -0,0 +1,19 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://httpbin.org/post", + files={ + "avatar": ("photo.jpg", b"\xff\xd8\xff\xe0fake-jpeg", "image/jpeg"), + "document": ("resume.pdf", b"%PDF-1.4 fake-pdf", "application/pdf"), + }, +) +async def upload_multiple(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() From 2020b5dd7d73702d05431276c657b3fab78ed5fc Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:47 +0700 Subject: [PATCH 13/20] feat: create upload_path.py --- examples/files/upload_path.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/files/upload_path.py diff --git a/examples/files/upload_path.py b/examples/files/upload_path.py new file mode 100644 index 0000000..0683307 --- /dev/null +++ b/examples/files/upload_path.py @@ -0,0 +1,20 @@ +from pathlib import Path + +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + +FILE = Path(__file__).parent / "sample.txt" + + +@app.post( + url="https://httpbin.org/post", + files={"file": FILE}, +) +async def upload_path(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() From 7e416de53760d6c8a38bc8ae5ea99750030483a1 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:50 +0700 Subject: [PATCH 14/20] feat: create upload_router.py --- examples/files/upload_router.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/files/upload_router.py diff --git a/examples/files/upload_router.py b/examples/files/upload_router.py new file mode 100644 index 0000000..0ad1558 --- /dev/null +++ b/examples/files/upload_router.py @@ -0,0 +1,20 @@ +from fasthttp import FastHTTP, Router +from fasthttp.response import Response + +router = Router(base_url="https://httpbin.org", prefix="/api") + + +@router.post( + url="/upload", + files={"file": b"Hello from router!"}, +) +async def upload(resp: Response) -> dict: + return resp.json() + + +app = FastHTTP() +app.include_router(router) + + +if __name__ == "__main__": + app.run() From d7c2a227d04f46516268826d24ffc9c7e88cf55f Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:52 +0700 Subject: [PATCH 15/20] feat: create upload_with_filename.py --- examples/files/upload_with_filename.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/files/upload_with_filename.py diff --git a/examples/files/upload_with_filename.py b/examples/files/upload_with_filename.py new file mode 100644 index 0000000..d335c71 --- /dev/null +++ b/examples/files/upload_with_filename.py @@ -0,0 +1,16 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://httpbin.org/post", + files={"file": ("report.csv", b"name,score\nAlice,95\nBob,87\n", "text/csv")}, +) +async def upload_with_meta(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() From 4aee26be43fe7c4040d9d80ea649a7b8c81357cc Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:38:55 +0700 Subject: [PATCH 16/20] feat: create upload_with_json.py --- examples/files/upload_with_json.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/files/upload_with_json.py diff --git a/examples/files/upload_with_json.py b/examples/files/upload_with_json.py new file mode 100644 index 0000000..b07de15 --- /dev/null +++ b/examples/files/upload_with_json.py @@ -0,0 +1,17 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.post( + url="https://httpbin.org/post", + json={"title": "My photo", "tags": ["nature", "sunset"]}, + files={"file": ("sunset.jpg", b"\xff\xd8\xff\xe0fake-jpeg", "image/jpeg")}, +) +async def upload_with_json(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() From 54d82adf068052847c9c8bcaab6e3903d4374669 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:39:08 +0700 Subject: [PATCH 17/20] update app.py --- fasthttp/app.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/fasthttp/app.py b/fasthttp/app.py index ed9fe6b..4882680 100644 --- a/fasthttp/app.py +++ b/fasthttp/app.py @@ -54,7 +54,7 @@ from .auth import BasicAuth, BearerAuth, DigestAuth, OAuth2ClientCredentials from .response import Response - from .types import DefaultEncoding, HTTPMethod, RequestsOptional + from .types import DefaultEncoding, FileUpload, HTTPMethod, RequestsOptional class FastHTTP: @@ -561,7 +561,7 @@ async def lifespan(app: FastHTTP): self.event_hooks = EventHooks() self.client = HTTPClient( - self.request_configs, + self.request_configs, # type: ignore self.logger, self.middleware_manager, self.security, @@ -672,6 +672,7 @@ def _add_route( params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -691,6 +692,7 @@ def decorator(func: Callable[..., object]) -> Callable[..., object]: params=params, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -804,6 +806,7 @@ def get( url: str, *, params: dict[str, Any] | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -816,6 +819,7 @@ def get( method="GET", url=url, params=params, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -831,6 +835,7 @@ def post( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -844,6 +849,7 @@ def post( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -859,6 +865,7 @@ def put( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -872,6 +879,7 @@ def put( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -887,6 +895,7 @@ def patch( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -900,6 +909,7 @@ def patch( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -915,6 +925,7 @@ def delete( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -928,6 +939,7 @@ def delete( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -942,6 +954,7 @@ def head( url: str, *, params: dict[str, Any] | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -954,6 +967,7 @@ def head( method="HEAD", url=url, params=params, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -968,6 +982,7 @@ def options( url: str, *, params: dict[str, Any] | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -980,6 +995,7 @@ def options( method="OPTIONS", url=url, params=params, + files=files, response_model=response_model, request_model=request_model, tags=tags, From 16fcfbf7b40927910f6ae894c8c2205a7f32da27 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:39:15 +0700 Subject: [PATCH 18/20] update client.py --- fasthttp/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fasthttp/client.py b/fasthttp/client.py index 8f3d642..3b84617 100644 --- a/fasthttp/client.py +++ b/fasthttp/client.py @@ -338,6 +338,7 @@ async def _execute_request( params=config.get("params", route.params), json=route.json, content=route.data, # type: ignore + files=route.files, # type: ignore timeout=timeout_config, follow_redirects=False, auth=resolve_auth(route.auth), From 0d02800819ea0bb8d0c054c2c864b53e5e82d405 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:39:22 +0700 Subject: [PATCH 19/20] update routing.py --- fasthttp/routing.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/fasthttp/routing.py b/fasthttp/routing.py index e2b448e..defe879 100644 --- a/fasthttp/routing.py +++ b/fasthttp/routing.py @@ -11,7 +11,7 @@ from .helpers.route_inspect import validate_handler from .helpers.routing import join_prefix as _join_prefix from .helpers.routing import resolve_url as _resolve_url -from .types import HTTPMethod # noqa: TC001 +from .types import FileUpload, HTTPMethod # noqa: TC001 if TYPE_CHECKING: from collections.abc import Callable @@ -55,6 +55,9 @@ class Route(BaseModel): data: object | None = None """Raw body or form data sent with the request.""" + files: FileUpload | None = None + """Files to upload as multipart/form-data.""" + response_model: type | None = None """Optional Pydantic model for validating the handler result.""" @@ -102,6 +105,7 @@ def __init__( params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -119,6 +123,7 @@ def __init__( params=params, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -140,6 +145,7 @@ class _RouteDef: params: dict[str, Any] | None json: dict[str, Any] | None data: object | None + files: FileUpload | None response_model: type[BaseModel] | None request_model: type[BaseModel] | None tags: list[str] @@ -228,6 +234,7 @@ def _add_route( params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -247,6 +254,7 @@ def decorator(func: Callable[..., object]) -> Callable[..., object]: params=params, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags or [], @@ -266,6 +274,7 @@ def get( url: str, *, params: dict[str, Any] | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -279,6 +288,7 @@ def get( method="GET", url=url, params=params, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -294,6 +304,7 @@ def post( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -308,6 +319,7 @@ def post( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -323,6 +335,7 @@ def put( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -337,6 +350,7 @@ def put( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -352,6 +366,7 @@ def patch( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -366,6 +381,7 @@ def patch( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -381,6 +397,7 @@ def delete( *, json: dict[str, Any] | None = None, data: object | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -395,6 +412,7 @@ def delete( url=url, json=json, data=data, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -409,6 +427,7 @@ def head( url: str, *, params: dict[str, Any] | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -422,6 +441,7 @@ def head( method="HEAD", url=url, params=params, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -436,6 +456,7 @@ def options( url: str, *, params: dict[str, Any] | None = None, + files: FileUpload | None = None, response_model: type[BaseModel] | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, @@ -449,6 +470,7 @@ def options( method="OPTIONS", url=url, params=params, + files=files, response_model=response_model, request_model=request_model, tags=tags, @@ -548,6 +570,7 @@ def build_routes( params=rd.params, json=rd.json, data=rd.data, + files=rd.files, response_model=rd.response_model, request_model=rd.request_model, tags=route_tags, From 98989f7d6f45b418bc13dcada56d93ca73b7cc21 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Mon, 22 Jun 2026 19:39:29 +0700 Subject: [PATCH 20/20] update types.py --- fasthttp/types.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fasthttp/types.py b/fasthttp/types.py index 5553fcf..e712cc4 100644 --- a/fasthttp/types.py +++ b/fasthttp/types.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +import io +from os import PathLike from typing import Annotated, Literal, TypeAlias, TypedDict from annotated_doc import Doc @@ -84,3 +88,17 @@ class RequestsOptional(TypedDict, total=False): """ ), ] + + +FileContent: TypeAlias = bytes | str | io.IOBase | io.RawIOBase | io.BufferedIOBase | io.TextIOBase | PathLike[str] + +FileTuple: TypeAlias = tuple[str, FileContent] | tuple[str, FileContent, str] | tuple[str, FileContent, str, dict[str, str]] + +FileUpload: TypeAlias = ( + FileContent + | FileTuple + | dict[str, FileContent] + | dict[str, FileTuple] + | list[tuple[str, FileContent]] + | list[tuple[str, FileTuple]] +)