Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add file uploads #4626

Merged
merged 30 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a93a9ea
Add first draft
Meschreiber Feb 9, 2024
b13c969
Update initial example for a single upload
Meschreiber Feb 9, 2024
29bb6b0
Add examples
Meschreiber Feb 12, 2024
504bc89
Add meta description
Meschreiber Feb 13, 2024
b183d9c
Add to list of enterprise features
Meschreiber Feb 13, 2024
067c2f6
Standardize caps
Meschreiber Feb 13, 2024
e45a515
Merge branch 'dev' into docs/file-uploads
Geal Feb 13, 2024
4948ea0
Copyedit
Meschreiber Feb 13, 2024
e4563b1
Apply suggestions from code review
Meschreiber Feb 15, 2024
fa7323e
Merge branch 'dev' into docs/file-uploads
Meschreiber Feb 16, 2024
ec12fa3
Remove from enterprise list (managed in monodocs now)
Meschreiber Feb 16, 2024
b2f8ddd
Update configuration information
Meschreiber Feb 16, 2024
51ec684
Merge branch 'dev' into docs/file-uploads
Meschreiber Feb 20, 2024
74bf5be
Callout private preview limitations
Meschreiber Feb 20, 2024
8da3c75
Merge branch 'dev' into docs/file-uploads
Meschreiber Feb 21, 2024
246c7dd
Add minVersion
Meschreiber Feb 23, 2024
42ab06f
Merge branch 'dev' into docs/file-uploads
Meschreiber Feb 23, 2024
9310020
Merge branch 'dev' into docs/file-uploads
BrynCooke Mar 5, 2024
c46a350
Bump minVersion
Meschreiber Mar 8, 2024
e6fbced
Update unsupported query modes
Meschreiber Apr 8, 2024
3a38b12
Add unsupported and supported usage examples
Meschreiber Apr 9, 2024
cbda633
Merge branch 'dev' into docs/file-uploads
abernix Apr 26, 2024
6ac203b
Merge branch 'dev' into docs/file-uploads
abernix May 8, 2024
c579d75
Merge branch 'dev' into docs/file-uploads
abernix May 13, 2024
dea67d4
Remove File uploads from nav
Meschreiber May 13, 2024
31a3768
Update frontmatter
Meschreiber May 13, 2024
d87080c
Condense callouts
Meschreiber May 13, 2024
172c8a4
Merge branch 'dev' into docs/file-uploads
Meschreiber May 13, 2024
9fa5247
Merge branch 'dev' into docs/file-uploads
abernix May 14, 2024
c927dcb
Merge branch 'dev' into docs/file-uploads
abernix May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/source/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@
"experimental"
]
],
"File uploads": [
"/executing-operations/file-uploads",
[
"enterprise",
"preview"
]
],
"GraphQL Subscriptions": {
"Subscriptions setup": [
"/executing-operations/subscription-support",
Expand Down
1 change: 1 addition & 0 deletions docs/source/enterprise-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Try out these Enterprise features for free with an [Enterprise trial](/graphos/o
## List of features

- **Real-time updates** via [GraphQL subscriptions](./executing-operations/subscription-support/)
- Support for [**file uploads** via multipart requests](./executing-operations/file-uploads)
- **Authentication of inbound requests** via [JSON Web Token (JWT)](./configuration/authn-jwt/)
- [**Authorization** of specific fields and types](./configuration/authorization) through the [`@requiresScopes`](./configuration/authorization#requiresscopes), [`@authenticated`](./configuration/authorization#authenticated), and [`@policy`](./configuration/authorization#policy) directives
- Redis-backed [**distributed caching** of query plans and persisted queries](./configuration/distributed-caching/) and [**subgraph entity caching**](./configuration/entity-caching/)
Expand Down
355 changes: 355 additions & 0 deletions docs/source/executing-operations/file-uploads.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
---
title: File uploads
subtitle: Receive file uploads with the Apollo Router
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
description: Configure the Apollo Router to receive file uploads using the GraphQL multipart request spec.
minVersion: X.x
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
---

<PreviewFeature />

<EnterpriseFeature />

Learn how to configure the Apollo Router to receive file uploads in client requests using the [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved

## About file uploads using multipart requests

A [multipart HTTP request](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) lets you include various data formats&mdash;such as text, file data, and JSON objects&mdash;in a single HTTP request. The [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec) uses multipart requests to upload files using arguments in a GraphQL mutation.
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved

### Example usage

Imagine you're building a platform where users can create posts with an image and title.
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
Your subgraph schema may include something like this:

```graphql showLineNumbers=false disableCopy
type Post {
id: ID!
title: String!
image: Upload!
}

type Mutation {
createPost(title: String!, image: Upload!): Post!
}
```

<Note>

Some GraphQL server implementations provide built-in support for handling file uploads, including an `Upload` scalar type.
For others, including the latest version of [Apollo Server](/apollo-server/), you must use external packages, such as [`graphql-upload`](https://www.npmjs.com/package/graphql-upload).
Refer to your subgraph library or package documentation for further information, including writing resolvers for uploaded files.

</Note>

When a client calls the `createPost` mutation, it can use variables to include the actual image file to upload:

```graphql showLineNumbers=false disableCopy
{
query: `
mutation CreatePost($title: String!, $image: Upload!) {
createPost(title: $title, image: $image) {
id
title
image
}
}
`,
variables: {
title: "My first post",
image: File // image.png
}
}
```

A request using the GraphQL multipart request spec would include the following as separate parts in a multipart request:

- the above operation definition
- the image file to upload
- a map between variables and files to upload

The exact requirements are documented in [Client usage requirements](#client-usage-requirements).

## File upload configuration and usage

To enable file uploads in clients, you must both [configure support in the Apollo Router](#configure-file-upload-support-in-the-router) and [ensure client usage conforms to requirements](#client-usage-requirements).
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved

### Configure file upload support in the router

By default, receiving client file uploads isn't enabled in the Apollo Router.
To enable file upload support, set the following fields in your `router.yaml` configuration file:

```yaml title="router.yaml" showLineNumbers="false"
preview_file_uploads:
enabled: true
protocols:
multipart:
enabled: true
mode: streaming
limits:
max_file_size: 5mb # default 1mb
max_files: 2 # default 4
```

#### Mode

The only supported `mode` is `streaming`.
That means the router doesn't retain uploaded files in memory during a request.
Streaming file uploads can be more memory-efficient, especially for large files, since it avoids loading the entire file into memory.

To ensure your operation is streamable, avoid nesting in file uploads.
For example, the following operation would not be streamable:

```graphql title="❌" disableCopy=true showLineNumbers=false
mutation UploadNestedFiles($files: [Upload!]!) {
uploadNestedFiles(files: $files) {
success
}
}

```

If a request cannot be fulfilled in a streaming fashion, the router returns the [`UPLOADS_OPERATION_CANNOT_STREAM`](#uploads_operation_cannot_stream) error.
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved

Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
#### Limits

The router includes default limits for file uploads to prevent denial-of-service attacks.
You can configure both the maximum file size and number of files to accept.
If a request exceeds limits, the router rejects them.
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved

#### Configuration reference

Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
<table class="field-table">
<thead>
<tr>
<th>Attribute/ <br/> Default value</th>
<th>Description</th>
<th>Valid Values</th>
</tr>
</thead>
<tbody>
<tr>
<td>

##### `enabled`

Default: `false`

</td>
<td>Flag to enable reception of client file uploads</td>
<td>boolean</td>

</tr>
<tr>
<td>

##### `protocols.multipart.enabled`
Default: `false`

</td>
<td>Flag to enable reception of multipart file uploads</td>
<td>boolean</td>
</tr>
<tr>
<td>

##### `protocols.multipart.mode`
Default: None
BrynCooke marked this conversation as resolved.
Show resolved Hide resolved

</td>
<td>Supported file upload mode</td>
<td>

`streaming`
(required value)

</td>
</tr>
<tr>
<td>

##### `protocols.multipart.limits.max_file_size`
Default: `1mb`
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved

</td>
<td>The maximum file size to accept</td>
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
<td>

Accepts values in a human-readable format; for example, `5kb` and `99mb` are acceptable values.

</td>
</tr>
<tr>
<td>

##### `protocols.multipart.limits.max_files`
Default: `4`

</td>
<td>The maximum number of files to accept</td>
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
<td>integer</td>
</tr>
</tbody>
</table>


### Client usage requirements

When calling a mutation with file uploads, the client must send the following HTTP parts in the following order:

1. The raw GraphQL operation
1. A map of file(s) to variable name(s)
1. The files to be uploaded, one HTTP request part per file

#### Example request payload

The following is an example of a multipart HTTP request payload that builds off the [example scenario](#example-usage):

```http disableCopy showLineNumbers=false title="Request payload"
--------------------------gc0p4Jq0M2Yt08jU534c0p
Content-Disposition: form-data; name="operations"

{ "query": "mutation CreatePost($title: String!, $image: Upload!) { createPost(title: $title, image: $image) { id } }", "variables": { "title": "My first post", "image": null } }
--------------------------gc0p4Jq0M2Yt08jU534c0p
Content-Disposition: form-data; name="map"

{ "0": ["variables.image"] }
--------------------------gc0p4Jq0M2Yt08jU534c0p
Content-Disposition: form-data; name="0"; filename="image.png"
Content-Type: image/png

[Binary image content here]
--------------------------gc0p4Jq0M2Yt08jU534c0p--
```

See below for an explanation of each part of the request payload:

- **`Content-Disposition: form-data; name="operations"`**
- The first part of the request must include the operation definition. This example specifies a mutation named `CreatePost` that accepts variables for a `title` and `image`.
- The `variables` object includes the title for the post and sets the `image` variable to `null` as the [multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec) requires for any variables that represent files to be uploaded.
- **`Content-Disposition: form-data; name="map"`**
- The second part of the request must include the mapping between the files to upload and the variables in the GraphQL operation.
- In this case, it maps the file in the request part with `name="0"` to `variables.image`. The map can use any key names you like&mdash;for example, `file1` instead of `0`&mdash;as long as the keys match the `name`s of the following request parts.
- **`Content-Disposition: form-data; name="0"; filename="image.png"`**
- The following part(s) contain the actual file(s) to be uploaded, with one file per part. The order of the files must match the order they're declared in the map in the second part of the request.
- In this case, there is only one file to upload, which has the name `image.png` and the appropriate content type (`image/png`)
- These parts also include actual file content&mdash;in this case, an image binary.

Each part of the request payload is separated by a boundary string (`gc0p4Jq0M2Yt08jU534c0p`) per the [multipart request format](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html).

Refer to the docs for your client library for further instructions.

- [Apollo Client (web)](/react/data/file-uploads/)
- [Apollo iOS](/ios/file-uploads/)
- [Apollo Kotlin](/kotlin/advanced/upload/)

Custom clients can be implemented following the [spec documentation](https://github.com/jaydenseric/graphql-multipart-request-spec).

## Security

Without additional security, HTTP multipart requests can be exploited as part of [cross-site request forgery](https://owasp.org/www-community/attacks/csrf) (CSRF) attacks.

The Apollo Router already has a mechanism to prevent these types of attacks, which is enabled by default. You should verify that your router hasn't disabled this mechanism before using file uploads. See [Cross-Site Request Forgery Prevention](../configuration/csrf) for details.

## Metrics for file uploads

Metrics in the Apollo Router for file uploads:

<table class="field-table metrics">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>

##### `apollo.router.operations.file_uploads`

</td>
<td>

Counter for the number of file uploads.

</td>
</tr>

<tr>
<td>

##### `apollo.router.operations.file_uploads.file_size`

</td>
<td>

Histogram for the size of uploaded files.

</td>
</tr>

<tr>
<td>

##### `apollo.router.operations.file_uploads.files`

</td>
<td>

Histogram for the number of uploaded files.

</td>
</tr>

</tbody>
</table>

## Error codes for file uploads

A file upload request may receive the following error responses:

<table class="field-table metrics">
<tr>
<th>Error Code</th>
<th>Description</th>
</tr>
<tr>
<td>

##### `UPLOADS_LIMITS_MAX_FILES_EXCEEDED`

</td>
<td>The number of files in the request exceeded the configured limit</td>
</tr>
<tr>
<td>

##### `UPLOADS_LIMITS_MAX_FILE_SIZE_EXCEEDED`

</td>
<td>A file exceeded the maximum configured file size.</td>
Meschreiber marked this conversation as resolved.
Show resolved Hide resolved
</tr>
<tr>
<td>

##### `UPLOADS_FILE_MISSING`

</td>
<td>The operation specified a file that was missing from the request</td>
</tr>
<tr>
<td>

##### `UPLOADS_OPERATION_CANNOT_STREAM`

</td>
<td>The request was invalid as it couldn't be streamed to the client</td>
</tr>
</table>


## Known limitations

### Unsupported query modes

When file uploads are enabled in the router, any operations using the [`@defer`](/graphos/operations/defer/) are unsupported.
Loading