Awesome
GraphQL multipart request specification
An interoperable multipart form field structure for GraphQL requests, used by various file upload client/server implementations.
It’s possible to implement:
- Nesting files anywhere within operations (usually in
variables
). - Operation batching.
- File deduplication.
- File upload streams in resolvers.
- Aborting file uploads in resolvers.
Multipart form field structure
An “operations object” is an Apollo GraphQL POST request (or array of requests if batching). An “operations path” is an object-path
string to locate a file within an operations object.
So operations can be resolved while the files are still uploading, the fields are ordered:
operations
: A JSON encoded operations object with files replaced withnull
.map
: A JSON encoded map of where files occurred in the operations. For each file, the key is the file multipart form field name and the value is an array of operations paths.- File fields: Each file extracted from the operations object with a unique, arbitrary field name.
Examples
Single file
Operations
{
query: `
mutation($file: Upload!) {
singleUpload(file: $file) {
id
}
}
`,
variables: {
file: File // a.txt
}
}
cURL request
curl localhost:3001/graphql \
-F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \
-F map='{ "0": ["variables.file"] }' \
-F 0=@a.txt
Request payload
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="map"
{ "0": ["variables.file"] }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="0"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
File list
Operations
{
query: `
mutation($files: [Upload!]!) {
multipleUpload(files: $files) {
id
}
}
`,
variables: {
files: [
File, // b.txt
File // c.txt
]
}
}
cURL request
curl localhost:3001/graphql \
-F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \
-F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \
-F 0=@b.txt \
-F 1=@c.txt
Request payload
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="operations"
{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="map"
{ "0": ["variables.files.0"], "1": ["variables.files.1"] }
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="0"; filename="b.txt"
Content-Type: text/plain
Bravo file content.
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="1"; filename="c.txt"
Content-Type: text/plain
Charlie file content.
--------------------------ec62457de6331cad--
Batching
Operations
[
{
query: `
mutation($file: Upload!) {
singleUpload(file: $file) {
id
}
}
`,
variables: {
file: File, // a.txt
},
},
{
query: `
mutation($files: [Upload!]!) {
multipleUpload(files: $files) {
id
}
}
`,
variables: {
files: [
File, // b.txt
File, // c.txt
],
},
},
];
cURL request
curl localhost:3001/graphql \
-F operations='[{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }]' \
-F map='{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] }' \
-F 0=@a.txt \
-F 1=@b.txt \
-F 2=@c.txt
Request payload
--------------------------627436eaefdbc285
Content-Disposition: form-data; name="operations"
[{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }]
--------------------------627436eaefdbc285
Content-Disposition: form-data; name="map"
{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] }
--------------------------627436eaefdbc285
Content-Disposition: form-data; name="0"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------627436eaefdbc285
Content-Disposition: form-data; name="1"; filename="b.txt"
Content-Type: text/plain
Bravo file content.
--------------------------627436eaefdbc285
Content-Disposition: form-data; name="2"; filename="c.txt"
Content-Type: text/plain
Charlie file content.
--------------------------627436eaefdbc285--
Security
GraphQL server authentication and security mechanisms are beyond the scope of this specification, which only covers a multipart form field structure for GraphQL requests.
Note that a GraphQL multipart request has the Content-Type multipart/form-data
; if a browser making such a request determines it meets the criteria for a “simple request” as defined in the Fetch specification for the Cross-Origin Resource Sharing (CORS) protocol, it won’t cause a CORS preflight request. GraphQL server authentication and security mechanisms must consider this to prevent Cross-Site Request Forgery (CSRF) attacks.
Implementations
Pull requests adding either experimental or mature implementations to these lists are welcome! Strikethrough means the project was renamed, deprecated, or no longer supports this spec out of the box (but might via an optional integration).
Client
- jaydenseric/graphql-react (JS: npm)
- jaydenseric/apollo-upload-client (JS: npm)
- jaydenseric/extract-files (JS: npm)
- nearform/graphql-hooks (JS: npm)
- klis87/redux-saga-requests-graphql (JS: npm)
- imolorhe/altair (JS: npm)
- haffdata/buoy (JS: npm)
- FormidableLabs/urql (JS: npm)
apollo-fetch-upload(JS: npm)- apollographql/apollo-ios (Swift: CocoaPods)
- apollographql/apollo-android (Java: Bintray)
- zino-app/graphql-flutter (Dart: Pub)
- samirelanduk/kirjava (Python: PyPi)
- DoctorJohn/aiogqlc (Python: PyPi)
- graphql-python/gql (Python: PyPi)
Server
- jaydenseric/graphql-upload (JS: npm)
- koresar/graphql-upload-minimal (JS: npm)
- dotansimha/graphql-yoga (JS: npm)
apollographql/apollo-server(JS: npm)jaydenseric/apollo-upload-server(JS: npm)- 99designs/gqlgen (Go: GitHub)
- jpascal/graphql-upload (Go: GitHub)
- jetruby/apollo_upload_server-ruby (Ruby: Gem)
- Ecodev/graphql-upload (PHP: Composer)
- rebing/graphql-laravel (PHP: Composer)
- nuwave/lighthouse (PHP: Composer)
- overblog/graphql-bundle (PHP: Composer)
- infinityloop-dev/graphpinator (PHP: Composer)
- lmcgartland/graphene-file-upload (Python: PyPi)
- strawberry-graphql/strawberry (Python: PyPi)
- graphql-java-kickstart/graphql-java-servlet (Java: Maven)
- ChilliCream/hotchocolate (C#: NuGet)
- async-graphql/async-graphql (Rust: Crates)