Skip to content

Commit 758ab94

Browse files
author
David Koblas
committed
Handle mutlipart/form-data
1 parent 59006c0 commit 758ab94

File tree

4 files changed

+214
-36
lines changed

4 files changed

+214
-36
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ h := handler.New(&handler.Config{
3636
})
3737
```
3838

39+
### Using Multipart Form Uploads
40+
41+
This handler supports th
42+
[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec).
43+
All file uploads will be made available as the following Scalar that you can add to your GraphQL schemas
44+
45+
```go
46+
var UploadScalar = graphql.NewScalar(graphql.ScalarConfig{
47+
Name: "Upload",
48+
ParseValue: func(value interface{}) interface{} {
49+
if v, ok := value.(*handler.MultipartFile); ok {
50+
return v
51+
}
52+
return nil
53+
},
54+
})
55+
```
56+
3957
### Details
4058

4159
The handler will accept requests with
@@ -70,6 +88,9 @@ depending on the provided `Content-Type` header.
7088
* **`application/graphql`**: The POST body will be parsed as GraphQL
7189
query string, which provides the `query` parameter.
7290

91+
* **`multipart/form-data`**: The POST body will be parsed as GraphQL
92+
query string, which provides the `operations` parameter.
93+
[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec)
7394

7495
### Examples
7596
- [golang-graphql-playground](https://github.com/graphql-go/playground)

handler.go

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package handler
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"io/ioutil"
7+
"mime/multipart"
68
"net/http"
79
"net/url"
10+
"strconv"
811
"strings"
912

1013
"github.com/graphql-go/graphql"
@@ -13,17 +16,24 @@ import (
1316
)
1417

1518
const (
16-
ContentTypeJSON = "application/json"
17-
ContentTypeGraphQL = "application/graphql"
18-
ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
19+
ContentTypeJSON = "application/json"
20+
ContentTypeGraphQL = "application/graphql"
21+
ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
22+
ContentTypeMultipartFormData = "multipart/form-data"
1923
)
2024

25+
type MultipartFile struct {
26+
File multipart.File
27+
Header *multipart.FileHeader
28+
}
29+
2130
type Handler struct {
2231
Schema *graphql.Schema
2332
pretty bool
2433
graphiql bool
2534
playground bool
2635
rootObjectFn RootObjectFn
36+
maxMemory int64
2737
}
2838
type RequestOptions struct {
2939
Query string `json:"query" url:"query" schema:"query"`
@@ -57,7 +67,7 @@ func getFromForm(values url.Values) *RequestOptions {
5767
}
5868

5969
// RequestOptions Parses a http.Request into GraphQL request options struct
60-
func NewRequestOptions(r *http.Request) *RequestOptions {
70+
func NewRequestOptions(r *http.Request, maxMemory int64) *RequestOptions {
6171
if reqOpt := getFromForm(r.URL.Query()); reqOpt != nil {
6272
return reqOpt
6373
}
@@ -95,6 +105,84 @@ func NewRequestOptions(r *http.Request) *RequestOptions {
95105

96106
return &RequestOptions{}
97107

108+
case ContentTypeMultipartFormData:
109+
if err := r.ParseMultipartForm(maxMemory); err != nil {
110+
// fmt.Printf("Parse Multipart Failed %v", err)
111+
return &RequestOptions{}
112+
}
113+
114+
// @TODO handle array case...
115+
116+
operationsParam := r.FormValue("operations")
117+
var opts RequestOptions
118+
if err := json.Unmarshal([]byte(operationsParam), &opts); err != nil {
119+
// fmt.Printf("Parse Operations Failed %v", err)
120+
return &RequestOptions{}
121+
}
122+
123+
mapParam := r.FormValue("map")
124+
mapValues := make(map[string]([]string))
125+
if len(mapParam) != 0 {
126+
if err := json.Unmarshal([]byte(mapParam), &mapValues); err != nil {
127+
// fmt.Printf("Parse map Failed %v", err)
128+
return &RequestOptions{}
129+
}
130+
}
131+
132+
variables := opts
133+
134+
for key, value := range mapValues {
135+
for _, v := range value {
136+
if file, header, err := r.FormFile(key); err == nil {
137+
138+
// Now set the path in ther variables
139+
var node interface{} = variables
140+
141+
parts := strings.Split(v, ".")
142+
last := parts[len(parts)-1]
143+
144+
for _, vv := range parts[:len(parts)-1] {
145+
// fmt.Printf("Doing vv=%s type=%T parts=%v\n", vv, node, parts)
146+
switch node.(type) {
147+
case RequestOptions:
148+
if vv == "variables" {
149+
node = opts.Variables
150+
} else {
151+
panic("Invalid top level tag")
152+
}
153+
case map[string]interface{}:
154+
node = node.(map[string]interface{})[vv]
155+
case []interface{}:
156+
if idx, err := strconv.ParseInt(vv, 10, 64); err == nil {
157+
node = node.([]interface{})[idx]
158+
} else {
159+
panic("Unable to lookup index")
160+
}
161+
default:
162+
panic(fmt.Errorf("Unknown type %T", node))
163+
}
164+
}
165+
166+
data := &MultipartFile{File: file, Header: header}
167+
168+
switch node.(type) {
169+
case map[string]interface{}:
170+
node.(map[string]interface{})[last] = data
171+
case []interface{}:
172+
if idx, err := strconv.ParseInt(last, 10, 64); err == nil {
173+
node.([]interface{})[idx] = data
174+
} else {
175+
panic("Unable to lookup index")
176+
}
177+
default:
178+
panic(fmt.Errorf("Unknown last type %T", node))
179+
}
180+
}
181+
}
182+
}
183+
184+
return &opts
185+
98186
case ContentTypeJSON:
99187
fallthrough
100188
default:
@@ -119,7 +207,7 @@ func NewRequestOptions(r *http.Request) *RequestOptions {
119207
// user-provided context.
120208
func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
121209
// get query
122-
opts := NewRequestOptions(r)
210+
opts := NewRequestOptions(r, h.maxMemory)
123211

124212
// execute graphql query
125213
params := graphql.Params{
@@ -182,14 +270,15 @@ type Config struct {
182270
GraphiQL bool
183271
Playground bool
184272
RootObjectFn RootObjectFn
273+
MaxMemory int64
185274
}
186275

187276
func NewConfig() *Config {
188277
return &Config{
189-
Schema: nil,
190-
Pretty: true,
191-
GraphiQL: true,
192-
Playground: false,
278+
Schema: nil,
279+
Pretty: true,
280+
GraphiQL: true,
281+
MaxMemory: 0,
193282
}
194283
}
195284

@@ -201,11 +290,17 @@ func New(p *Config) *Handler {
201290
panic("undefined GraphQL schema")
202291
}
203292

293+
maxMemory := p.MaxMemory
294+
if maxMemory == 0 {
295+
maxMemory = 32 << 20 // 32MB
296+
}
297+
204298
return &Handler{
205299
Schema: p.Schema,
206300
pretty: p.Pretty,
207301
graphiql: p.GraphiQL,
208302
playground: p.Playground,
209303
rootObjectFn: p.RootObjectFn,
304+
maxMemory: maxMemory,
210305
}
211306
}

handler_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package handler_test
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"io/ioutil"
8+
"mime/multipart"
79
"net/http"
810
"net/http/httptest"
911
"reflect"
@@ -196,3 +198,63 @@ func TestHandler_BasicQuery_WithRootObjFn(t *testing.T) {
196198
t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result))
197199
}
198200
}
201+
202+
func TestHandler_Post(t *testing.T) {
203+
expected := &graphql.Result{
204+
Data: map[string]interface{}{
205+
"hero": map[string]interface{}{
206+
"name": "R2-D2",
207+
},
208+
},
209+
}
210+
queryString := `{"query":"query HeroNameQuery { hero { name } }"}`
211+
212+
req, _ := http.NewRequest("POST", "/graphql", strings.NewReader(queryString))
213+
req.Header.Set("Content-Type", "application/json")
214+
215+
h := handler.New(&handler.Config{
216+
Schema: &testutil.StarWarsSchema,
217+
})
218+
result, resp := executeTest(t, h, req)
219+
if resp.Code != http.StatusOK {
220+
t.Fatalf("unexpected server response %v", resp.Code)
221+
}
222+
if !reflect.DeepEqual(result, expected) {
223+
t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result))
224+
}
225+
}
226+
227+
func TestHandler_Multipart_Basic(t *testing.T) {
228+
body := &bytes.Buffer{}
229+
230+
writer := multipart.NewWriter(body)
231+
232+
expected := &graphql.Result{
233+
Data: map[string]interface{}{
234+
"hero": map[string]interface{}{
235+
"name": "R2-D2",
236+
},
237+
},
238+
}
239+
queryString := `{"query":"query HeroNameQuery { hero { name } }"}`
240+
241+
writer.WriteField("operations", queryString)
242+
err := writer.Close()
243+
if err != nil {
244+
t.Fatalf("unexpected writer fail %v", err)
245+
}
246+
247+
req, err := http.NewRequest("POST", "/graphql", body)
248+
req.Header.Set("Content-Type", writer.FormDataContentType())
249+
250+
h := handler.New(&handler.Config{
251+
Schema: &testutil.StarWarsSchema,
252+
})
253+
result, resp := executeTest(t, h, req)
254+
if resp.Code != http.StatusOK {
255+
t.Fatalf("unexpected server response %v", resp.Code)
256+
}
257+
if !reflect.DeepEqual(result, expected) {
258+
t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result))
259+
}
260+
}

0 commit comments

Comments
 (0)