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.

Note The traditional usage is to upload files as part of a GraphQL request and reference the uploaded file during the GraphQL resolution.
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:

Note This is an example of a non-normative note.

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.

Note When the operations part is first received the Server can start processing the GraphQL Request before the entire payload has been uploaded.

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.
Note Supplying a filename for the content-disposition in an embedded part is optional. However, the Server MAY choose to reject requests based on the presence of a filename.
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.
Note The map part is a legacy Part from V2 of the specification. If the Server only implements V3 of this specification it ignores the map part so that clients can support sending backwards compatible V2/V3 requests.

3Schema

There are no requirements on the GraphQL Schema by this specification.

Note The Server MAY choose to use a GraphQL scalar type to reference the embedded parts. The Server MAY have multiple different scalars with different meanings in the Schema.
Example № 6scalar Upload
scalar File
scalar Part

4Execution

ExecuteRequest(httpRequest)
  1. If the Server finds that httpRequest is a multipart/form-data request:
    1. Return ExecuteMultipartRequest(httpRequest)
  2. Else
    1. Return ExecuteGraphQLOverHTTPRequest(httpRequest)
ExecuteMultipartRequest(multipartRequest)
  1. Let partsMap be the result of parsing the multipartRequest into a map of NamePart
  2. Let embeddedPartsMap be an empty Map.
  3. For each partsMap entry partNamepartValue
    1. If partName is operations
      1. Return ProcessGraphQLRequest(partValue, embeddedPartsMap)
    2. Else If partName is map
      1. If this server supports V2 via backwards compatability
        1. The Server must handle multipartRequest according to V2 of this spec
      2. Else ignore this Part
    3. Else Store the entry partNamepartValue in embeddedPartsMap
ProcessGraphQLRequest(query, embeddedPartsMap)
  1. Process this according to ExecuteGraphQLRequest(query)
  2. 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.
    1. 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.
Note ProcessGraphQLRequest() depends on async data being pulled from embeddedPartsMap. The Server shouldn’t fail if a required embedded part isn’t available until the entire multipartRequest has been read.
Note This specification doesn’t put any specific requirements on how the server knows that additional data is required or what key to use to look this up. However, as mentioned, the expected flow is that a special 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

Note if the Client‘s GraphQL request doesn’t reference any embedded parts this isn’t an error.

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 names the Server MUST return an error for the request.

Note The Client MAY send multiple files with the same filename
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.
Note This allows the V3 Client to not worry about the supported specification version of the Server as both V2 and V3 Servers will be able to handle the request.
GraphQL Request
Example № 16mutation($file: Upload!) {
   upload(file: $file)
}
Variables
Example № 17{
  "file": "fileA"
}
Map Part
Example № 18{
  "fileA": [
    "variables.file"
  ]
}
Note In the above example, note that the key in the map part SHOULD be the same as the name of the embedded part that contains the relevant additional data for clarity.

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

§Index

  1. embedded part
  2. ExecuteMultipartRequest
  3. ExecuteRequest
  4. map part
  5. multipart/form-data
  6. operations part
  7. ProcessGraphQLRequest
  1. 1GraphQL Multipart Request
    1. 1.1Overview
    2. 1.2Conformance
    3. 1.3Non-Normative Portions
  2. 2multipart/form-data Format
    1. 2.1Parts
      1. 2.1.1Operations Part
      2. 2.1.2Embedded Parts
      3. 2.1.3Map Part
  3. 3Schema
  4. 4Execution
    1. 4.1Error Handling
      1. 4.1.1Missing Operations Part
      2. 4.1.2Missing Embedded Parts
      3. 4.1.3Duplicate Embedded Parts
  5. 5Backwards Compatability
    1. 5.1Client Backwards Compatibility
    2. 5.2Server Backwards Compatibility
  6. 6Examples
    1. 6.1Single File
    2. 6.2Multiple Files
    3. 6.3Variables And File Reuse
    4. 6.4V2 Client → V3 Backwards Compatible Server
    5. 6.5V3 Backwards Compatible Client → V2 Server
    6. 6.6V3 Backwards Compatible Client → Non Backwards Compatible V3 Server
  7. §Index