├── .github └── funding.yml ├── .prettierrc.json ├── sync-vs-async-graphql-multipart-request-middleware.sketch ├── sync-vs-async-graphql-multipart-request-middleware.svg └── readme.md /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: jaydenseric 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never" 3 | } 4 | -------------------------------------------------------------------------------- /sync-vs-async-graphql-multipart-request-middleware.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaydenseric/graphql-multipart-request-spec/HEAD/sync-vs-async-graphql-multipart-request-middleware.sketch -------------------------------------------------------------------------------- /sync-vs-async-graphql-multipart-request-middleware.svg: -------------------------------------------------------------------------------- 1 | 2 | Sync vs async GraphQL multipart request middleware 3 | 4 | 5 | 6 | GraphQL API upload 7 | 8 | 9 | 10 | Cloud storage upload 11 | 12 | 13 | 14 | 15 | 16 | 1mb.png 17 | 18 | 19 | 20 | 21 | 22 | 2mb.png 23 | 24 | 25 | 26 | 27 | 28 | 3mb.png 29 | 30 | 31 | 32 | Response 33 | 34 | 35 | Receive 36 | request 37 | 38 | 39 | Run 40 | resolver 41 | 42 | 43 | Sync: 44 | Store files temporarily on API server (filesystem or memory). 45 | 46 | 47 | 48 | 49 | 50 | 1mb.png 51 | 52 | 53 | 54 | 55 | 56 | 2mb.png 57 | 58 | 59 | 60 | 61 | 62 | 3mb.png 63 | 64 | 65 | 66 | Receive 67 | request 68 | 69 | 70 | Response 71 | 72 | 73 | Run 74 | resolver 75 | 76 | 77 | Async: 78 | Pass file upload streams into resolver. 79 | 80 | 81 | Sync vs async GraphQL 82 | multipart request middleware 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # GraphQL multipart request specification 2 | 3 | An interoperable [multipart form](https://tools.ietf.org/html/rfc7578) field structure for GraphQL requests, used by various file upload client/server implementations. 4 | 5 | It’s possible to implement: 6 | 7 | - Nesting files anywhere within operations (usually in `variables`). 8 | - Operation batching. 9 | - File deduplication. 10 | - File upload streams in resolvers. 11 | - Aborting file uploads in resolvers. 12 | 13 | ![Sync vs async GraphQL multipart request middleware](sync-vs-async-graphql-multipart-request-middleware.svg) 14 | 15 | ## Multipart form field structure 16 | 17 | An “operations object” is an [Apollo GraphQL POST request](https://www.apollographql.com/docs/apollo-server/workflow/requests/#post-requests) (or array of requests if batching). An “operations path” is an [`object-path`](https://npm.im/object-path) string to locate a file within an operations object. 18 | 19 | So operations can be resolved while the files are still uploading, the fields are ordered: 20 | 21 | 1. `operations`: A JSON encoded operations object with files replaced with `null`. 22 | 2. `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. 23 | 3. File fields: Each file extracted from the operations object with a unique, arbitrary field name. 24 | 25 | ## Examples 26 | 27 | ### Single file 28 | 29 | #### Operations 30 | 31 | ```js 32 | { 33 | query: ` 34 | mutation($file: Upload!) { 35 | singleUpload(file: $file) { 36 | id 37 | } 38 | } 39 | `, 40 | variables: { 41 | file: File // a.txt 42 | } 43 | } 44 | ``` 45 | 46 | #### cURL request 47 | 48 | ```shell 49 | curl localhost:3001/graphql \ 50 | -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ 51 | -F map='{ "0": ["variables.file"] }' \ 52 | -F 0=@a.txt 53 | ``` 54 | 55 | #### Request payload 56 | 57 | ``` 58 | --------------------------cec8e8123c05ba25 59 | Content-Disposition: form-data; name="operations" 60 | 61 | { "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } } 62 | --------------------------cec8e8123c05ba25 63 | Content-Disposition: form-data; name="map" 64 | 65 | { "0": ["variables.file"] } 66 | --------------------------cec8e8123c05ba25 67 | Content-Disposition: form-data; name="0"; filename="a.txt" 68 | Content-Type: text/plain 69 | 70 | Alpha file content. 71 | 72 | --------------------------cec8e8123c05ba25-- 73 | ``` 74 | 75 | ### File list 76 | 77 | #### Operations 78 | 79 | ```js 80 | { 81 | query: ` 82 | mutation($files: [Upload!]!) { 83 | multipleUpload(files: $files) { 84 | id 85 | } 86 | } 87 | `, 88 | variables: { 89 | files: [ 90 | File, // b.txt 91 | File // c.txt 92 | ] 93 | } 94 | } 95 | ``` 96 | 97 | #### cURL request 98 | 99 | ```shell 100 | curl localhost:3001/graphql \ 101 | -F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \ 102 | -F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \ 103 | -F 0=@b.txt \ 104 | -F 1=@c.txt 105 | ``` 106 | 107 | #### Request payload 108 | 109 | ``` 110 | --------------------------ec62457de6331cad 111 | Content-Disposition: form-data; name="operations" 112 | 113 | { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } } 114 | --------------------------ec62457de6331cad 115 | Content-Disposition: form-data; name="map" 116 | 117 | { "0": ["variables.files.0"], "1": ["variables.files.1"] } 118 | --------------------------ec62457de6331cad 119 | Content-Disposition: form-data; name="0"; filename="b.txt" 120 | Content-Type: text/plain 121 | 122 | Bravo file content. 123 | 124 | --------------------------ec62457de6331cad 125 | Content-Disposition: form-data; name="1"; filename="c.txt" 126 | Content-Type: text/plain 127 | 128 | Charlie file content. 129 | 130 | --------------------------ec62457de6331cad-- 131 | ``` 132 | 133 | ### Batching 134 | 135 | #### Operations 136 | 137 | ```js 138 | [ 139 | { 140 | query: ` 141 | mutation($file: Upload!) { 142 | singleUpload(file: $file) { 143 | id 144 | } 145 | } 146 | `, 147 | variables: { 148 | file: File, // a.txt 149 | }, 150 | }, 151 | { 152 | query: ` 153 | mutation($files: [Upload!]!) { 154 | multipleUpload(files: $files) { 155 | id 156 | } 157 | } 158 | `, 159 | variables: { 160 | files: [ 161 | File, // b.txt 162 | File, // c.txt 163 | ], 164 | }, 165 | }, 166 | ]; 167 | ``` 168 | 169 | #### cURL request 170 | 171 | ```shell 172 | curl localhost:3001/graphql \ 173 | -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] } }]' \ 174 | -F map='{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] }' \ 175 | -F 0=@a.txt \ 176 | -F 1=@b.txt \ 177 | -F 2=@c.txt 178 | ``` 179 | 180 | #### Request payload 181 | 182 | ``` 183 | --------------------------627436eaefdbc285 184 | Content-Disposition: form-data; name="operations" 185 | 186 | [{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }] 187 | --------------------------627436eaefdbc285 188 | Content-Disposition: form-data; name="map" 189 | 190 | { "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] } 191 | --------------------------627436eaefdbc285 192 | Content-Disposition: form-data; name="0"; filename="a.txt" 193 | Content-Type: text/plain 194 | 195 | Alpha file content. 196 | 197 | --------------------------627436eaefdbc285 198 | Content-Disposition: form-data; name="1"; filename="b.txt" 199 | Content-Type: text/plain 200 | 201 | Bravo file content. 202 | 203 | --------------------------627436eaefdbc285 204 | Content-Disposition: form-data; name="2"; filename="c.txt" 205 | Content-Type: text/plain 206 | 207 | Charlie file content. 208 | 209 | --------------------------627436eaefdbc285-- 210 | ``` 211 | 212 | ## Security 213 | 214 | GraphQL server authentication and security mechanisms are beyond the scope of this specification, which only covers a multipart form field structure for GraphQL requests. 215 | 216 | Note that a GraphQL multipart request has the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) `multipart/form-data`; if a browser making such a request determines it meets the criteria for a “[simple request](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)” as defined in the [Fetch specification](https://fetch.spec.whatwg.org) for the [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (CORS) protocol, it won’t cause a [CORS preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request). GraphQL server authentication and security mechanisms must consider this to prevent [Cross-Site Request Forgery](https://developer.mozilla.org/en-US/docs/Glossary/CSRF) (CSRF) attacks. 217 | 218 | ## Implementations 219 | 220 | 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). 221 | 222 | ### Client 223 | 224 | - [jaydenseric/graphql-react](https://github.com/jaydenseric/graphql-react) (JS: [npm](https://npm.im/graphql-react)) 225 | - [jaydenseric/apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) (JS: [npm](https://npm.im/apollo-upload-client)) 226 | - [jaydenseric/extract-files](https://github.com/jaydenseric/extract-files) (JS: [npm](https://npm.im/extract-files)) 227 | - [nearform/graphql-hooks](https://github.com/nearform/graphql-hooks) (JS: [npm](https://npm.im/graphql-hooks)) 228 | - [klis87/redux-saga-requests-graphql](https://github.com/klis87/redux-saga-requests/tree/master/packages/redux-saga-requests-graphql) (JS: [npm](https://npm.im/redux-saga-requests-graphql)) 229 | - [imolorhe/altair](https://github.com/imolorhe/altair) (JS: [npm](https://npm.im/altair-static)) 230 | - [haffdata/buoy](https://github.com/haffdata/buoy) (JS: [npm](https://npm.im/@buoy/client)) 231 | - [FormidableLabs/urql](https://github.com/FormidableLabs/urql) (JS: [npm](https://npm.im/@urql/exchange-multipart-fetch)) 232 | - [~~apollo-fetch-upload~~](https://github.com/apollographql/apollo-fetch/tree/master/packages/apollo-fetch-upload) (JS: [npm](https://npm.im/apollo-fetch-upload)) 233 | - [GnRlLeclerc/go-graphql-client](https://github.com/GnRlLeclerc/go-graphql-client) (Go: [GitHub](https://github.com/GnRlLeclerc/go-graphql-client)) 234 | - [apollographql/apollo-ios](https://github.com/apollographql/apollo-ios) (Swift: [CocoaPods](https://cocoapods.org/pods/Apollo)) 235 | - [apollographql/apollo-android](https://github.com/apollographql/apollo-android) (Java: [Bintray](https://bintray.com/apollographql/android)) 236 | - [zino-app/graphql-flutter](https://github.com/zino-app/graphql-flutter) (Dart: [Pub](https://pub.dev/packages/graphql)) 237 | - [samirelanduk/kirjava](https://github.com/samirelanduk/kirjava) (Python: [PyPi](https://pypi.org/project/kirjava)) 238 | - [DoctorJohn/aiogqlc](https://github.com/DoctorJohn/aiogqlc) (Python: [PyPi](https://pypi.org/project/aiogqlc)) 239 | - [graphql-python/gql](https://github.com/graphql-python/gql) (Python: [PyPi](https://pypi.org/project/gql)) 240 | 241 | ### Server 242 | 243 | - [jaydenseric/graphql-upload](https://github.com/jaydenseric/graphql-upload) (JS: [npm](https://npm.im/graphql-upload)) 244 | - [koresar/graphql-upload-minimal](https://github.com/koresar/graphql-upload-minimal) (JS: [npm](https://npm.im/graphql-upload-minimal)) 245 | - [dotansimha/graphql-yoga](https://github.com/dotansimha/graphql-yoga) (JS: [npm](https://npm.im/@graphql-yoga/common)) 246 | - [~~apollographql/apollo-server~~](https://github.com/apollographql/apollo-server) (JS: [npm](https://npm.im/apollo-server)) 247 | - [~~jaydenseric/apollo-upload-server~~](https://github.com/jaydenseric/apollo-upload-server) (JS: [npm](https://npm.im/apollo-upload-server)) 248 | - [99designs/gqlgen](https://github.com/99designs/gqlgen) (Go: [GitHub](https://github.com/99designs/gqlgen)) 249 | - [jpascal/graphql-upload](https://github.com/jpascal/graphql-upload) (Go: [GitHub](https://github.com/jpascal/graphql-upload)) 250 | - [jetruby/apollo_upload_server-ruby](https://github.com/jetruby/apollo_upload_server-ruby) (Ruby: [Gem](https://rubygems.org/gems/apollo_upload_server)) 251 | - [Ecodev/graphql-upload](https://github.com/Ecodev/graphql-upload) (PHP: [Composer](https://packagist.org/packages/ecodev/graphql-upload)) 252 | - [rebing/graphql-laravel](https://github.com/rebing/graphql-laravel) (PHP: [Composer](https://packagist.org/packages/rebing/graphql-laravel)) 253 | - [nuwave/lighthouse](https://github.com/nuwave/lighthouse) (PHP: [Composer](https://packagist.org/packages/nuwave/lighthouse)) 254 | - [overblog/graphql-bundle](https://github.com/overblog/GraphQLBundle) (PHP: [Composer](https://packagist.org/packages/overblog/graphql-bundle)) 255 | - [infinityloop-dev/graphpinator](https://github.com/infinityloop-dev/graphpinator) (PHP: [Composer](https://packagist.org/packages/infinityloop-dev/graphpinator)) 256 | - [lmcgartland/graphene-file-upload](https://github.com/lmcgartland/graphene-file-upload) (Python: [PyPi](https://pypi.org/project/graphene-file-upload)) 257 | - [strawberry-graphql/strawberry](https://github.com/strawberry-graphql/strawberry) (Python: [PyPi](https://pypi.org/project/strawberry-graphql)) 258 | - [graphql-java-kickstart/graphql-java-servlet](https://github.com/graphql-java-kickstart/graphql-java-servlet) (Java: [Maven](https://mvnrepository.com/artifact/com.graphql-java/graphql-java-servlet)) 259 | - [ChilliCream/hotchocolate](https://github.com/ChilliCream/hotchocolate) (C#: [NuGet](https://www.nuget.org/packages/HotChocolate)) 260 | - [async-graphql/async-graphql](https://github.com/async-graphql/async-graphql) (Rust: [Crates](https://crates.io/crates/async-graphql)) 261 | --------------------------------------------------------------------------------