GraphQL Multipart Request V3
1GraphQL Multipart Request
1.1Overview
This specification describes how a GraphQL Over HTTP compliant Server and Client can support passing additional data alongside a GraphQL request using multipart/form-data.
Example № 1POST https://example.com/graphql
content-type: multipart/form-data; boundary=--boundary
--boundary
Content-Disposition: form-data; name="operations"
{ "query": "mutation { upload(file: \"fileA\") }" }
--boundary
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--boundary--
1.2Conformance
A conforming implementation of GraphQL Multipart Request must fulfill all normative requirements. Conformance requirements are described in this document via both descriptive assertions and key words with clearly defined meanings.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “ RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative portions of this document are to be interpreted as described in IETF RFC 2119. These key words may appear in lowercase and still retain their meaning unless explicitly declared as non-normative.
A conforming implementation of Multipart Request may provide additional functionality, but must not where explicitly disallowed or would otherwise result in non-conformance.
1.3Non-Normative Portions
All contents of this document are normative except portions explicitly declared as non-normative.
Examples in this document are non-normative, and are presented to aid understanding of introduced concepts and the behavior of normative portions of the specification. Examples are either introduced explicitly in prose (e.g. “for example”) or are set apart in example or counter-example blocks, like this:
Example № 2This is an example of a non-normative example.
Counter Example № 3This is an example of a non-normative counter-example.
Notes in this document are non-normative, and are presented to clarify intent, draw attention to potential edge-cases and pit-falls, and answer common questions that arise during implementation. Notes are either introduced explicitly in prose (e.g. “Note: “) or are set apart in a note block, like this:
2multipart/form-data Format
multipart/form-data is defined by RFC7578
2.1Parts
A GraphQL MultipartRequest is made up of multiple Parts. These Parts MAY be arranged in any order, however a Client SHOULD send the operations part first for optimal performance. The Server MUST support receiving the parts in any order.
2.1.1Operations Part
- operations part
- A Part with the Name
operations
. This part includes the GraphQL payload as defined in GraphQL Over HTTP. - There MUST be exactly one operations part.
Example № 4POST https://example.com/graphql
content-type: multipart/form-data; boundary=--boundary
--boundary
Content-Disposition: form-data; name="operations"
{ "query": "query { field }" }
--boundary--
2.1.2Embedded Parts
- embedded part
- There MAY be zero or more Parts in our multipart/form-data request which include additional data.
- These Parts MUST not have the name
operations
.
Example № 5POST https://example.com/graphql
content-type: multipart/form-data; boundary=--boundary
--boundary
Content-Disposition: form-data; name="operations"
{ "query": "mutation { upload(file: \"fileA\") upload(file: \"fileB\") }" }
--boundary
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--boundary
Content-Disposition: form-data; name="fileB"
Content-Type: text/plain
Beta file content.
--boundary--
2.1.3Map Part
- map part
- There MAY be a single Part with the name
map
. - A server implementing only V3 of this specification MUST ignore this
map
Part.
3Schema
There are no requirements on the GraphQL Schema by this specification.
scalar
type to reference the embedded parts. The Server MAY have multiple different scalar
s with different meanings in the Schema.Example № 6scalar Upload
scalar File
scalar Part
4Execution
- If the Server finds that httpRequest is a multipart/form-data request:
- Return ExecuteMultipartRequest(httpRequest)
- Else
- Return ExecuteGraphQLOverHTTPRequest(httpRequest)
- Let partsMap be the result of parsing the multipartRequest into a map of Name → Part
- Let embeddedPartsMap be an empty Map.
- For each partsMap entry partName → partValue
- If partName is
operations
- Return ProcessGraphQLRequest(partValue, embeddedPartsMap)
- Else If partName is
map
- If this server supports V2 via backwards compatability
- The Server must handle multipartRequest according to V2 of this spec
- Else ignore this Part
- If this server supports V2 via backwards compatability
- Else Store the entry partName → partValue in embeddedPartsMap
- If partName is
- Process this according to ExecuteGraphQLRequest(query)
- If the Server finds a piece of the GraphQL request that expects additional data it must pull this additional data from the embeddedPartsMap as referenced by the Name and provide it to the internal resolver.
- If the Server encounters an error when looking up the additional data in the embeddedPartsMap it MUST bubble up this error from the location in the GraphQL query that requested it.
scalar
is created where the client can set a key to reference the Name of one of the embedded parts.Example № 7scalar Attachment
type Mutation {
upload(attachment: Attachment): Boolean
}
Example № 8POST https://example.com/graphql
content-type: multipart/form-data; boundary=--boundary
--boundary
Content-Disposition: form-data; name="operations"
{ "query": "mutation { upload(attachment: \"fileA\") }" }
--boundary
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--boundary--
4.1Error Handling
4.1.1Missing Operations Part
If the Client sends a multipart/form-data request without an operations part, the Server MUST respond according to the GraphQL Over HTTP specification when receiving an invalid GraphQL request.
Request
Counter Example № 9POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
Response
Example № 10{
"errors": [
{
"message": "Missing GraphQL Operation"
}
]
}
4.1.2Missing Embedded Parts
The request is invalid if it is missing referenced embedded parts. This can happen when the Client sends a non multipart/form-data request or sends a multipart/form-data request missing referenced embedded parts. The Server MUST handle these errors as outlined in the GraphQL Spec: HandleFieldError().
Request
Counter Example № 11POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation { upload(file: \"fileA\") }" }
--------------------------cec8e8123c05ba25
Response
Example № 12{
"data": {
"upload": null
},
"errors": [
{
"message": "Missing fileA",
"location": [
{
"line": 1,
"column": 37
}
],
"path": [
"upload"
]
}
]
}
4.1.3Duplicate Embedded Parts
If the Client sends a multipart/form-data request with duplicate name
s the Server MUST return an error for the request.
Invalid Request
Counter Example № 13POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation { upload(file: \"fileA\") }" }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content Again.
--------------------------cec8e8123c05ba25--
Response
Example № 14{
"errors": [
{
"message": "Found duplicate parts: fileA"
}
]
}
Valid Request
Example № 15POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation { upload(file: \"fileA\") }" }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileB"; filename="a.txt"
Content-Type: text/plain
Alpha file content Again.
--------------------------cec8e8123c05ba25--
5Backwards Compatability
With V2 of this spec
5.1Client Backwards Compatibility
V3 Clients MAY choose to make requests that are compatible with both V2 AND V3 of this specification.
A cross compatible request is a V2 compliant request where instead of using null
for additional data. The embedded part name is used instead.
- V2 Servers will overwrite the embedded part name when processing the map part
- V3 Servers will ignore the map part and process the request like normal.
GraphQL Request
Example № 16mutation($file: Upload!) {
upload(file: $file)
}
Variables
Example № 17{
"file": "fileA"
}
Map Part
Example № 18{
"fileA": [
"variables.file"
]
}
5.2Server Backwards Compatibility
V3 Servers MAY choose to support V2 requests of this specification. If a backwards compatible V3 Server receives a request with a map part it must implement the execution flow defined by V2 of this spec. The file key’s values MUST be ignored and substituted sequentially with the contents of the map part. If no map part is present, it MUST implement the flow described in ExecuteMultipartRequest().
6Examples
Schema
This is the schema for all of the examples in this section.
Example № 19scalar Upload
type Mutation {
upload(file: Upload!): String
}
6.1Single File
GraphQL Request
Example № 20mutation {
upload(file: "fileA")
}
Payload
Example № 21POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation { upload(file: \"fileA\") }" }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
cURL Request
Example № 22curl localhost:3001/graphql \
-F operations='{ "query": "mutation { upload(file: \"fileA\") }" }' \
-F fileA=@a.txt
6.2Multiple Files
GraphQL Request
Example № 23mutation {
a: upload(file: "fileA")
b: upload(file: "fileB")
}
Payload
Example № 24POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation { a: upload(file: \"fileA\") b: upload(file: \"fileB\") }" }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileB"; filename="b.mpg"
Content-Type: video/mpeg
Beta file content.
--------------------------cec8e8123c05ba25--
cURL Request
Example № 25curl localhost:3001/graphql \
-F operations='{ "query": "mutation { a: upload(file: \"fileA\") b: upload(file: \"fileB\") }" }' \
-F fileA=@a.txt \
-F fileB=@b.mpg
6.3Variables And File Reuse
GraphQL Request
Example № 26mutation($file: Upload!) {
a: upload(file: $file)
b: upload(file: $file)
}
Variables
Example № 27{
"file": "fileA"
}
Payload
Example № 28POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation($file: Upload!) { a: upload(file: $file) b: upload(file: $file) }", "variables": { "file": "fileA" } }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
cURL Request
Example № 29curl localhost:3001/graphql \
-F operations='{ "query": "mutation($file: Upload!) { a: upload(file: $file) b: upload(file: $file) }", "variables": { "file": "fileA" } }' \
-F fileA=@a.txt
6.4V2 Client → V3 Backwards Compatible Server
The Backwards Compatible V3 Server will find the map
Part and use it to replace the null
value in the operations
json before executing the request via its standard flow.
GraphQL Request
Example № 30mutation($file: Upload!) {
upload(file: $file)
}
Variables
Example № 31{
"file": null
}
Map Part
Example № 32{
"fileA": [
"variables.file"
]
}
Payload
Example № 33POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation($file: Upload!) { upload(file: $file) }", "variables": { "file": null } }
Content-Disposition: form-data; name="map"
{ "fileA": ["variables.file"] }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
cURL Request
Example № 34curl localhost:3001/graphql \
-F operations='{ "query": "mutation($file: Upload!) { upload(file: $file) }", "variables": { "file": null } }' \
-F map='{ "fileA": ["variables.file"] }' \
-F fileA=@a.txt
6.5V3 Backwards Compatible Client → V2 Server
The V3 Client has already filled in the value for the file
variable in the operations part json. The V2 Server will ignore this and still pull the value from the map part.
GraphQL Request
Example № 35mutation($file: Upload!) {
upload(file: $file)
}
Variables
Example № 36{
"file": "fileA"
}
Map Part
Example № 37{
"fileA": [
"variables.file"
]
}
Payload
Example № 38POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation($file: Upload!) { upload(file: $file) }", "variables": { "file": "fileA" } }
Content-Disposition: form-data; name="map"
{ "fileA": ["variables.file"] }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
cURL Request
Example № 39curl localhost:3001/graphql \
-F operations='{ "query": "mutation($file: Upload!) { upload(file: $file) }", "variables": { "file": "fileA" } }' \
-F map='{ "fileA": ["variables.file"] }' \
-F fileA=@a.txt
6.6V3 Backwards Compatible Client → Non Backwards Compatible V3 Server
The V3 Server will ignore the map part however the client has already filled in the fileA
key in the variables
json. The V3 Server will use this value.
GraphQL Request
Example № 40mutation($file: Upload!) {
upload(file: $file)
}
Variables
Example № 41{
"file": "fileA"
}
Map Part
Example № 42{
"fileA": [
"variables.file"
]
}
Payload
Example № 43POST /graphql HTTP/1.1
Host: localhost:3001
content-type: multipart/form-data; boundary=------------------------cec8e8123c05ba25
content-length: ?
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation($file: Upload!) { upload(file: $file) }", "variables": { "file": "fileA" } }
Content-Disposition: form-data; name="map"
{ "fileA": ["variables.file"] }
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="fileA"; filename="a.txt"
Content-Type: text/plain
Alpha file content.
--------------------------cec8e8123c05ba25--
cURL Request
Example № 44curl localhost:3001/graphql \
-F operations='{ "query": "mutation($file: Upload!) { upload(file: $file) }", "variables": { "file": "fileA" } }' \
-F map='{ "fileA": ["variables.file"] }' \
-F fileA=@a.txt