Skip to content

Commit c61bfc0

Browse files
committed
rewrite based on feedback
1 parent 928bd9c commit c61bfc0

File tree

1 file changed

+55
-118
lines changed

1 file changed

+55
-118
lines changed

src/pages/learn/file-uploads.mdx

Lines changed: 55 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,76 @@
1-
# Handling File Uploads
1+
# Handling File Uploads in GraphQL
22

3-
GraphQL doesn't natively support file uploads. The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic
4-
and historically assumed `application/json`, but the evolving [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/)
5-
introduces support for additional media types: `application/graphql-response+json`.
3+
GraphQL was not designed with file uploads in mind. While it’s technically possible to implement them, doing so requires
4+
extending the transport layer and introduces several risks, both in security and reliability.
65

7-
Since uploading files typically requires `multipart/form-data`, adding upload capabilities still
8-
means extending the HTTP layer yourself. This guide explains how to handle file uploads using
9-
[`graphql-http`](https://github.com/graphql/graphql-http), a minimal, spec-compliant GraphQL server implementation for JavaScript.
6+
This guide explains why file uploads via GraphQL are problematic and presents safer alternatives.
107

11-
## Why file uploads require extra work
8+
## Why uploads are challenging
129

13-
A standard GraphQL request sends a query or mutation and optional variables as JSON. But file
14-
uploads require binary data, which JSON can't represent. Instead, clients typically use
15-
`multipart/form-data`, the same encoding used for HTML file forms. This format is incompatible
16-
with how GraphQL servers like `graphql-http` handle requests by default.
10+
The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic and assumes requests are encoded as JSON.
11+
File uploads, by contrast, require `multipart/form-data` encoding to transfer binary data—something JSON can’t handle.
1712

18-
To bridge this gap, the GraphQL community developed a convention: the [GraphQL multipart
19-
request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). This
20-
approach allows files to be uploaded as part of a GraphQL mutation, with the server handling the
21-
`multipart/form-data` payload and injecting the uploaded file into the appropriate variable.
13+
Supporting uploads over GraphQL usually involves adopting community conventions, like the
14+
[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). While useful in some
15+
environments, these solutions often introduce complexity, fragility, and security risks.
2216

23-
## The multipart upload format
17+
## Risks to be aware of
2418

25-
The multipart spec defines a three-part request format:
19+
### Memory exhaustion from repeated variables
2620

27-
- `operations`: A JSON string representing the GraphQL operation
28-
- `map`: A JSON object that maps file field name to variable paths
29-
- One or more files: Attached to the form using the same field names referenced in the `map`
21+
GraphQL operations allow the same variable to be referenced multiple times. If a file upload variable is used more than
22+
once, its stream may be consumed multiple times—or worse, not at all. This can lead to unpredictable behavior or denial of service (DoS).
3023

31-
### Example
24+
### Stream leaks on failed operations
3225

33-
```graphql
34-
mutation UploadFile($file: Upload!) {
35-
uploadFile(file: $file) {
36-
filename
37-
mimetype
38-
}
39-
}
40-
```
26+
GraphQL executes in phases: validation, then execution. If an error occurs during validation or authorization, your
27+
server might never reach the resolver that consumes a file stream. If file streams are left unconsumed, memory usage can
28+
spike, potentially exhausting server resources.
4129

42-
And the corresponding `map` field:
30+
### Cross-Site Request Forgery (CSRF)
4331

44-
```json
45-
{
46-
"0": ["variables.file"]
47-
}
48-
```
32+
`multipart/form-data` is classified as a “simple” request by CORS and does not trigger preflight checks. Without strict CSRF
33+
protections, malicious sites may be able to upload files on behalf of unsuspecting users.
34+
35+
### Oversized or excess payloads
36+
37+
Attackers can upload arbitrarily large files or extra files not referenced in the GraphQL operation. If your server accepts and
38+
buffers these files in memory, you may face reliability issues or be vulnerable to resource exhaustion.
39+
40+
### Untrusted file metadata
41+
42+
Uploaded file names, MIME types, and even contents are arbitrary and should be treated as untrusted input. Failing to sanitize
43+
file names can lead to path traversal vulnerabilities. Assuming a file’s MIME type is safe can lead to parsing exploits.
44+
45+
## Recommendation: Use signed URLs
4946

50-
The server is responsible for parsing the multipart body, interpreting the `map`, and replacing
51-
variable paths with the corresponding file streams.
47+
The most secure and scalable approach is to **avoid uploading files through GraphQL entirely**. Instead:
5248

53-
## Implementing uploads with graphql-http
49+
1. Use a GraphQL mutation to request a signed upload URL from your storage provider (e.g., Amazon S3).
50+
2. Upload the file directly from the client using that URL.
51+
3. Submit a second mutation to associate the uploaded file with your application’s data.
5452

55-
The `graphql-http` package doesn’t handle multipart requests out of the box. To support file
56-
uploads, you’ll need to:
53+
This approach isolates the file upload concern to infrastructure purpose-built for it, while keeping GraphQL focused on structured data.
5754

58-
1. Parse the multipart form request.
59-
2. Map the uploaded file(s) to GraphQL variables.
60-
3. Inject those into the request body before passing it to `createHandler()`.
55+
## If you still choose to support uploads
6156

62-
Here's how to do it in an Express-based server using JavaScript and the [`busboy`](https://www.npmjs.com/package/busboy),
63-
a popular library for parsing `multipart/form-data`.
57+
If your application truly requires file uploads through GraphQL, proceed with caution. At a minimum, you should:
6458

65-
### Example: Express + graphql-http + busboy
59+
- Use a well-maintained implementation of the
60+
[GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
61+
- Enforce a rule that upload variables are only referenced once.
62+
- Always stream uploads to disk or cloud storage—never buffer them in memory.
63+
- Apply strict request size limits and validate all fields.
64+
- Treat file names, types, and contents as untrusted data.
65+
66+
## Example (not recommended for production)
67+
68+
The example below demonstrates how uploads could be wired up using Express, `graphql-http`, and busboy.
69+
It’s included only to illustrate the mechanics and is not production-ready.
70+
71+
<Callout type="warning" emoji="⚠️">
72+
We strongly discourage using this code in production.
73+
</Callout>
6674

6775
```js
6876
import express from 'express';
@@ -110,74 +118,3 @@ app.post('/graphql', (req, res, next) => {
110118

111119
app.listen(4000);
112120
```
113-
114-
This example:
115-
116-
- Parses `multipart/form-data` uploads.
117-
- Extracts GraphQL query and variables from the `operations` field.
118-
- Inserts file streams in place of `Upload` variables.
119-
- Passes the modified request to `graphql-http`.
120-
121-
## Defining the upload scalar
122-
123-
The GraphQL schema must include a custom scalar type for uploaded files:
124-
125-
```graphql
126-
scalar Upload
127-
128-
extend type Mutation {
129-
uploadFile(file: Upload!): FileMetadata
130-
}
131-
132-
type FileMetadata {
133-
filename: String!
134-
mimetype: String!
135-
}
136-
```
137-
138-
In your resolvers, treat `file` as a readable stream:
139-
140-
```js
141-
export const resolvers = {
142-
Upload: GraphQLScalarType, // implement as needed, or treat as opaque in resolver
143-
Mutation: {
144-
uploadFile: async (_, { file }) => {
145-
const chunks = [];
146-
for await (const chunk of file) {
147-
chunks.push(chunk);
148-
}
149-
// process or store the file as needed
150-
return {
151-
filename: 'uploaded-file.txt',
152-
mimetype: 'text/plain',
153-
};
154-
}
155-
}
156-
};
157-
```
158-
159-
You can define `Upload` as a passthrough scalar if your server middleware already
160-
handles file parsing:
161-
162-
```js
163-
import { GraphQLScalarType } from 'graphql';
164-
165-
export const Upload = new GraphQLScalarType({
166-
name: 'Upload',
167-
serialize: () => { throw new Error('Upload serialization unsupported'); },
168-
parseValue: value => value,
169-
parseLiteral: () => { throw new Error('Upload literals unsupported'); }
170-
});
171-
```
172-
173-
## Best practices
174-
175-
- Streaming: Don’t read entire files into memory. Instead, stream files to disk or an external
176-
storage service. This reduces memory pressure and improves
177-
scalability.
178-
- Security: Always validate file types, restrict maximum file sizes, and sanitize filenames to prevent
179-
path traversal or injection vulnerabilities.
180-
- Alternatives: For large files or more scalable architectures, consider using pre-signed URLs
181-
with an object storage service like S3. The client uploads the file directly, and the GraphQL
182-
mutation receives the file URL instead.
183-
- Client support: Use a client library that supports the GraphQL multipart request specification.

0 commit comments

Comments
 (0)