Back to Strawberry

File Upload

docs/guides/file-upload.md

0.315.37.7 KB
Original Source

File Upload

All Strawberry integrations support multipart uploads as described in the GraphQL multipart request specification. This includes support for uploading single files as well as lists of files.

Security

Note that multipart file upload support is disabled by default in all integrations. Before enabling multipart file upload support, make sure you address the security implications outlined in the specification. Usually, this entails enabling CSRF protection in your server framework (e.g., the CsrfViewMiddleware middleware in Django).

To enable file upload support, pass multipart_uploads_enabled=True to your integration's view class. Refer to the integration-specific documentation for more details on how to do this.

Upload Scalar

Uploads can be used in mutations via the Upload scalar. The type passed at runtime depends on the integration:

IntegrationType
AIOHTTPio.BytesIO
ASGIstarlette.datastructures.UploadFile
Channelsdjango.core.files.uploadedfile.UploadedFile
Djangodjango.core.files.uploadedfile.UploadedFile
FastAPIfastapi.UploadFile
Flaskwerkzeug.datastructures.FileStorage
Quartquart.datastructures.FileStorage
Sanicsanic.request.File
Starlettestarlette.datastructures.UploadFile

In order to have the correct runtime type in resolver type annotations (which also gives you proper type checking with mypy/pyright), you can set a scalar override based on the integrations above. For example with Starlette:

python
import strawberry
from starlette.datastructures import UploadFile
from strawberry.file_uploads import UploadDefinition

schema = strawberry.Schema(
    query=Query,
    mutation=Mutation,
    scalar_overrides={UploadFile: UploadDefinition},
)

With this configuration, you can use the framework's upload type directly in your resolvers:

python
@strawberry.type
class Mutation:
    @strawberry.mutation
    async def read_file(self, file: UploadFile) -> str:
        return (await file.read()).decode("utf-8")

This gives you proper IDE autocomplete and type checking, since the type annotation matches the actual runtime type.

ASGI / FastAPI / Starlette

Since these integrations use asyncio for communication, the resolver must be async.

Additionally, these servers rely on the python-multipart package, which is not included by Strawberry by default. It can be installed directly, or, for convenience, it is included in extras: strawberry-graphql[asgi] (for ASGI/Starlette) or strawberry-graphql[fastapi] (for FastAPI). For example:

  • if using Pip, pip install 'strawberry-graphql[fastapi]'
  • if using uv, uv add 'strawberry-graphql[fastapi]'
  • if using Poetry, strawberry-graphql = { version = "...", extras = ["fastapi"] } in pyproject.toml.

Example:

python
import typing
import strawberry
from strawberry.file_uploads import Upload


@strawberry.input
class FolderInput:
    files: typing.List[Upload]


@strawberry.type
class Mutation:
    @strawberry.mutation
    async def read_file(self, file: Upload) -> str:
        return (await file.read()).decode("utf-8")

    @strawberry.mutation
    async def read_files(self, files: typing.List[Upload]) -> typing.List[str]:
        contents = []
        for file in files:
            content = (await file.read()).decode("utf-8")
            contents.append(content)
        return contents

    @strawberry.mutation
    async def read_folder(self, folder: FolderInput) -> typing.List[str]:
        contents = []
        for file in folder.files:
            content = (await file.read()).decode("utf-8")
            contents.append(content)
        return contents

Sanic / Flask / Django / Channels / AIOHTTP

Example:

python
import typing
import strawberry
from strawberry.file_uploads import Upload


@strawberry.input
class FolderInput:
    files: typing.List[Upload]


@strawberry.type
class Mutation:
    @strawberry.mutation
    def read_file(self, file: Upload) -> str:
        return file.read().decode("utf-8")

    @strawberry.mutation
    def read_files(self, files: typing.List[Upload]) -> typing.List[str]:
        contents = []
        for file in files:
            content = file.read().decode("utf-8")
            contents.append(content)
        return contents

    @strawberry.mutation
    def read_folder(self, folder: FolderInput) -> typing.List[str]:
        contents = []
        for file in folder.files:
            contents.append(file.read().decode("utf-8"))
        return contents

Sending file upload requests

The tricky part is sending the HTTP request from the client because it must follow the GraphQL multipart request specifications mentioned above.

The multipart/form-data POST request's data must include:

  • operations key for GraphQL request with query and variables
  • map key with mapping some multipart-data to exact GraphQL variable
  • and other keys for multipart-data which contains binary data of files

Assuming you have your schema up and running, here there are some requests examples:

Sending one file

shell
curl localhost:8000/graphql \
  -F operations='{ "query": "mutation($file: Upload!){ readFile(file: $file) }", "variables": { "file": null } }' \
  -F map='{ "file": ["variables.file"] }' \
  -F [email protected]

Sending a list of files

shell
curl localhost:8000/graphql \
  -F operations='{ "query": "mutation($files: [Upload!]!) { readFiles(files: $files) }", "variables": { "files": [null, null] } }' \
  -F map='{"file1": ["variables.files.0"], "file2": ["variables.files.1"]}' \
  -F [email protected] \
  -F [email protected]

Sending nested files

shell
curl localhost:8000/graphql \
  -F operations='{ "query": "mutation($folder: FolderInput!) { readFolder(folder: $folder) }", "variables": {"folder": {"files": [null, null]}} }' \
  -F map='{"file1": ["variables.folder.files.0"], "file2": ["variables.folder.files.1"]}' \
  -F [email protected] \
  -F [email protected]