|
1 |
| -# Handling File Uploads |
| 1 | +# Handling File Uploads in GraphQL |
2 | 2 |
|
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. |
6 | 5 |
|
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. |
10 | 7 |
|
11 |
| -## Why file uploads require extra work |
| 8 | +## Why uploads are challenging |
12 | 9 |
|
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. |
17 | 12 |
|
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. |
22 | 16 |
|
23 |
| -## The multipart upload format |
| 17 | +## Risks to be aware of |
24 | 18 |
|
25 |
| -The multipart spec defines a three-part request format: |
| 19 | +### Memory exhaustion from repeated variables |
26 | 20 |
|
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). |
30 | 23 |
|
31 |
| -### Example |
| 24 | +### Stream leaks on failed operations |
32 | 25 |
|
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. |
41 | 29 |
|
42 |
| -And the corresponding `map` field: |
| 30 | +### Cross-Site Request Forgery (CSRF) |
43 | 31 |
|
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 |
49 | 46 |
|
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: |
52 | 48 |
|
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. |
54 | 52 |
|
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. |
57 | 54 |
|
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 |
61 | 56 |
|
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: |
64 | 58 |
|
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> |
66 | 74 |
|
67 | 75 | ```js
|
68 | 76 | import express from 'express';
|
@@ -110,74 +118,3 @@ app.post('/graphql', (req, res, next) => {
|
110 | 118 |
|
111 | 119 | app.listen(4000);
|
112 | 120 | ```
|
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