-from fastapi import FastAPI, File
+from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
-async def create_file(*, file: bytes = File(...)):
+async def create_file(file: bytes = File(...)):
return {"file_size": len(file)}
+
+
+@app.post("/uploadfile/")
+async def create_upload_file(file: UploadFile = File(...)):
+ return {"filename": file.filename}
-from fastapi import FastAPI, File, Form
+from fastapi import FastAPI, File, Form, UploadFile
app = FastAPI()
@app.post("/files/")
-async def create_file(*, file: bytes = File(...), token: str = Form(...)):
- return {"file_size": len(file), "token": token}
+async def create_file(
+ file: bytes = File(...), fileb: UploadFile = File(...), token: str = Form(...)
+):
+ return {
+ "file_size": len(file),
+ "token": token,
+ "fileb_content_type": fileb.content_type,
+ }
## Import `File`
-Import `File` from `fastapi`:
+Import `File` and `UploadFile` from `fastapi`:
```Python hl_lines="1"
{!./src/request_files/tutorial001.py!}
{!./src/request_files/tutorial001.py!}
```
-The files will be uploaded as form data and you will receive the contents as `bytes`.
-
!!! info
`File` is a class that inherits directly from `Form`.
!!! info
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameters or body (JSON) parameters.
+The files will be uploaded as "form data".
+
+If you declare the type of your *path operation function* parameter as `bytes`, **FastAPI** will read the file for you and you will receive the contents as `bytes`.
+
+Have in mind that this means that the whole contents will be stored in memory. This will work well for small files.
+
+But there are several cases in where you might benefit from using `UploadFile`.
+
+
+## `File` parameters with `UploadFile`
+
+Define a `File` parameter with a type of `UploadFile`:
+
+```Python hl_lines="12"
+{!./src/request_files/tutorial001.py!}
+```
+
+Using `UploadFile` has several advantages over `bytes`:
+
+* It uses a "spooled" file:
+ * A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk.
+* This means that it will work well for large files like images, videos, large binaries, etc. All without consuming all the memory.
+* You can get metadata from the uploaded file.
+* It has a <a href="https://docs.python.org/3/glossary.html#term-file-like-object" target="_blank">file-like</a> `async` interface.
+* It exposes an actual Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" target="_blank">`SpooledTemporaryFile`</a> object that you can pass directly to other libraries that expect a file-like object.
+
+
+### `UploadFile`
+
+`UploadFile` has the following attributes:
+
+* `filename`: A `str` with the original file name that was uploaded (e.g. `myimage.jpg`).
+* `content_type`: A `str` with the content type (MIME type / media type) (e.g. `image/jpeg`).
+* `file`: A <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" target="_blank">`SpooledTemporaryFile`</a> (a <a href="https://docs.python.org/3/glossary.html#term-file-like-object" target="_blank">file-like</a> object). This is the actual Python file that you can pass directly to other functions or libraries that expect a "file-like" object.
+
+
+`UploadFile` has the following `async` methods. They all call the corresponding file methods underneath (using the internal `SpooledTemporaryFile`).
+
+* `write(data)`: Writes `data` (`str` or `bytes`) to the file.
+* `read(size)`: Reads `size` (`int`) bytes/characters of the file.
+* `seek(offset)`: Goes to the byte position `offset` (`int`) in the file.
+ * E.g., `myfile.seek(0)` would go to the start of the file.
+ * This is especially useful if you run `myfile.read()` once and then need to read the contents again.
+* `close()`: Closes the file.
+
+As all these methods are `async` methods, you need to "await" them.
+
+For example, inside of an `async` *path operation function* you can get the contents with:
+
+```Python
+contents = await myfile.read()
+```
+
+If you are inside of a normal `def` *path operation function*, you can access the `UploadFile.file` directly, for example:
+
+```Python
+contents = myfile.file.read()
+```
+
+!!! note "`async` Technical Details"
+ When you use the `async` methods, **FastAPI** runs the file methods in a threadpool and awaits for them.
+
+
+!!! note "Starlette Technical Details"
+ **FastAPI**'s `UploadFile` inherits directly from **Starlette**'s `UploadFile`, but adds some necessary parts to make it compatible with **Pydantic** and the other parts of FastAPI.
+
## "Form Data"?
The way HTML forms (`<form></form>`) sends the data to the server normally uses a "special" encoding for that data, it's different from JSON.
Create file and form parameters the same way you would for `Body` or `Query`:
-```Python hl_lines="7"
+```Python hl_lines="8"
{!./src/request_forms_and_files/tutorial001.py!}
```
The files and form fields will be uploaded as form data and you will receive the files and form fields.
+And you can declare some of the files as `bytes` and some as `UploadFile`.
+
!!! warning
You can declare multiple `File` and `Form` parameters in a path operation, but you can't also declare `Body` fields that you expect to receive as JSON, as the request will have the body encoded using `multipart/form-data` instead of `application/json`.
from .routing import APIRouter
from .params import Body, Path, Query, Header, Cookie, Form, File, Security, Depends
from .exceptions import HTTPException
+from .datastructures import UploadFile
--- /dev/null
+from typing import Any, Callable, Iterable, Type
+
+from starlette.datastructures import UploadFile as StarletteUploadFile
+
+
+class UploadFile(StarletteUploadFile):
+ @classmethod
+ def __get_validators__(cls: Type["UploadFile"]) -> Iterable[Callable]:
+ yield cls.validate
+
+ @classmethod
+ def validate(cls: Type["UploadFile"], v: Any) -> Any:
+ if not isinstance(v, StarletteUploadFile):
+ raise ValueError(f"Expected UploadFile, received: {type(v)}")
+ return v
from pydantic.schema import get_annotation_from_schema
from pydantic.utils import lenient_issubclass
from starlette.concurrency import run_in_threadpool
+from starlette.datastructures import UploadFile
from starlette.requests import Headers, QueryParams, Request
param_supported_types = (
else:
values[field.name] = deepcopy(field.default)
continue
+ if (
+ isinstance(field.schema, params.File)
+ and lenient_issubclass(field.type_, bytes)
+ and isinstance(value, UploadFile)
+ ):
+ value = await value.read()
v_, errors_ = field.validate(value, values, loc=("body", field.alias))
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
return values, errors
+def get_schema_compatible_field(*, field: Field) -> Field:
+ if lenient_issubclass(field.type_, UploadFile):
+ return Field(
+ name=field.name,
+ type_=bytes,
+ class_validators=field.class_validators,
+ model_config=field.model_config,
+ default=field.default,
+ required=field.required,
+ alias=field.alias,
+ schema=field.schema,
+ )
+ return field
+
+
def get_body_field(*, dependant: Dependant, name: str) -> Field:
flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params:
first_param = flat_dependant.body_params[0]
embed = getattr(first_param.schema, "embed", None)
if len(flat_dependant.body_params) == 1 and not embed:
- return first_param
+ return get_schema_compatible_field(field=first_param)
model_name = "Body_" + name
BodyModel = create_model(model_name)
for f in flat_dependant.body_params:
- BodyModel.__fields__[f.name] = f
+ BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
required = any(True for f in flat_dependant.body_params if f.required)
if any(isinstance(f.schema, params.File) for f in flat_dependant.body_params):
BodySchema: Type[params.Body] = params.File
from starlette import routing
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
-from starlette.formparsers import UploadFile
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import compile_path, get_name, request_response
raw_body = await request.form()
form_fields = {}
for field, value in raw_body.items():
- if isinstance(value, UploadFile):
- form_fields[field] = await value.read()
- else:
- form_fields[field] = value
+ form_fields[field] = value
if form_fields:
body = form_fields
else:
--- /dev/null
+import pytest
+from fastapi import UploadFile
+
+
+def test_upload_file_invalid():
+ with pytest.raises(ValueError):
+ UploadFile.validate("not a Starlette UploadFile")
"required": True,
},
}
- }
+ },
+ "/uploadfile/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Create Upload File Post",
+ "operationId": "create_upload_file_uploadfile__post",
+ "requestBody": {
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_create_upload_file"
+ }
+ }
+ },
+ "required": True,
+ },
+ }
+ },
},
"components": {
"schemas": {
"file": {"title": "File", "type": "string", "format": "binary"}
},
},
+ "Body_create_upload_file": {
+ "title": "Body_create_upload_file",
+ "required": ["file"],
+ "type": "object",
+ "properties": {
+ "file": {"title": "File", "type": "string", "format": "binary"}
+ },
+ },
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
response = client.post("/files/", files={"file": open(path, "rb")})
assert response.status_code == 200
assert response.json() == {"file_size": default_pydantic_max_size + 1}
+
+
+def test_post_upload_file(tmpdir):
+ path = os.path.join(tmpdir, "test.txt")
+ with open(path, "wb") as file:
+ file.write(b"<file content>")
+
+ client = TestClient(app)
+ response = client.post("/uploadfile/", files={"file": open(path, "rb")})
+ assert response.status_code == 200
+ assert response.json() == {"filename": "test.txt"}
import os
+from pathlib import Path
from starlette.testclient import TestClient
"schemas": {
"Body_create_file": {
"title": "Body_create_file",
- "required": ["file", "token"],
+ "required": ["file", "fileb", "token"],
"type": "object",
"properties": {
"file": {"title": "File", "type": "string", "format": "binary"},
+ "fileb": {"title": "Fileb", "type": "string", "format": "binary"},
"token": {"title": "Token", "type": "string"},
},
},
"loc": ["body", "file"],
"msg": "field required",
"type": "value_error.missing",
- }
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
]
}
token_required = {
"detail": [
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
{
"loc": ["body", "token"],
"msg": "field required",
"type": "value_error.missing",
- }
+ },
]
}
+# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]}
+
file_and_token_required = {
"detail": [
{
"msg": "field required",
"type": "value_error.missing",
},
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
{
"loc": ["body", "token"],
"msg": "field required",
assert response.json() == token_required
-def test_post_file_and_token(tmpdir):
- path = os.path.join(tmpdir, "test.txt")
- with open(path, "wb") as file:
- file.write(b"<file content>")
+def test_post_files_and_token(tmpdir):
+ patha = Path(tmpdir) / "test.txt"
+ pathb = Path(tmpdir) / "testb.txt"
+ patha.write_text("<file content>")
+ pathb.write_text("<file b content>")
client = TestClient(app)
response = client.post(
- "/files/", data={"token": "foo"}, files={"file": open(path, "rb")}
+ "/files/",
+ data={"token": "foo"},
+ files={
+ "file": patha.open("rb"),
+ "fileb": ("testb.txt", pathb.open("rb"), "text/plain"),
+ },
)
assert response.status_code == 200
- assert response.json() == {"file_size": 14, "token": "foo"}
+ assert response.json() == {
+ "file_size": 14,
+ "token": "foo",
+ "fileb_content_type": "text/plain",
+ }