├── readme.md └── handler.go /readme.md: -------------------------------------------------------------------------------- 1 | # batched-graphql-handler 2 | An [http handler](https://golang.org/pkg/net/http/#Handler) to use [graphql-go](https://github.com/neelance/graphql-go) with a graphql client that supports batching like [graphql-query-batcher](https://github.com/nicksrandall/graphql-query-batcher) or [apollo client](https://github.com/apollostack/apollo-client). 3 | 4 | ### Notes 5 | - This handler only supports [batched queries](https://dev-blog.apollodata.com/query-batching-in-apollo-63acfd859862#.p7459gedh). It doesn't support all of the apollo stack featuers. 6 | - If you'd like to support gzip in your handler, I suggest wrapping this handler with [GZIP Handler](https://github.com/NYTimes/gziphandler) by NY Times 7 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package apollo 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | 12 | graphql "github.com/neelance/graphql-go" 13 | ) 14 | 15 | const ( 16 | ContentTypeJSON = "application/json" 17 | ContentTypeGraphQL = "application/graphql" 18 | ContentTypeFormURLEncoded = "application/x-www-form-urlencoded" 19 | ) 20 | 21 | type Handler struct { 22 | Schema *graphql.Schema 23 | Pretty bool 24 | } 25 | 26 | type RequestOptions struct { 27 | Query string `json:"query" url:"query"` 28 | Variables map[string]interface{} `json:"variables" url:"variables"` 29 | OperationName string `json:"operationName" url:"operationName"` 30 | } 31 | 32 | // a workaround for getting`variables` as a JSON string 33 | type requestOptionsCompatibility struct { 34 | Query string `json:"query" url:"query"` 35 | Variables string `json:"variables" url:"variables"` 36 | OperationName string `json:"operationName" url:"operationName"` 37 | } 38 | 39 | func getFromForm(values url.Values) *RequestOptions { 40 | query := values.Get("query") 41 | if query != "" { 42 | // get variables map 43 | var variables map[string]interface{} 44 | variablesStr := values.Get("variables") 45 | json.Unmarshal([]byte(variablesStr), &variables) 46 | 47 | return &RequestOptions{ 48 | Query: query, 49 | Variables: variables, 50 | OperationName: values.Get("operationName"), 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 | var output interface{} 59 | 60 | // TODO: improve Content-Type handling 61 | contentTypeStr := r.Header.Get("Content-Type") 62 | contentTypeTokens := strings.Split(contentTypeStr, ";") 63 | contentType := contentTypeTokens[0] 64 | 65 | switch contentType { 66 | case ContentTypeFormURLEncoded: 67 | if err := r.ParseForm(); err != nil { 68 | http.Error(w, err.Error(), http.StatusBadRequest) 69 | return 70 | } 71 | 72 | query := getFromForm(r.PostForm) 73 | if query == nil { 74 | http.Error(w, "Could not parse graphql params from form.", http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | output = h.Schema.Exec(r.Context(), query.Query, query.OperationName, query.Variables) 79 | case ContentTypeGraphQL: 80 | body, err := ioutil.ReadAll(r.Body) 81 | if err == nil { 82 | http.Error(w, err.Error(), http.StatusInternalServerError) 83 | return 84 | } 85 | output = h.Schema.Exec(r.Context(), string(body), "", make(map[string]interface{})) 86 | case ContentTypeJSON: 87 | body, err := ioutil.ReadAll(r.Body) 88 | if err != nil { 89 | http.Error(w, err.Error(), http.StatusBadRequest) 90 | return 91 | } 92 | 93 | requestOptions := resolveJSON(body) 94 | switch opts := requestOptions.(type) { 95 | case *RequestOptions: 96 | output = h.Schema.Exec(r.Context(), opts.Query, opts.OperationName, opts.Variables) 97 | case []*RequestOptions: 98 | var results []*graphql.Response 99 | for i := range opts { 100 | results = append(results, h.Schema.Exec(r.Context(), opts[i].Query, opts[i].OperationName, opts[i].Variables)) 101 | } 102 | output = results 103 | default: 104 | log.Printf("bad type: %T", opts) 105 | http.Error(w, "unrecognized RequestOptions type", http.StatusBadRequest) 106 | return 107 | } 108 | default: 109 | http.Error(w, "unrecognized content-type header: `"+contentType+"`", http.StatusBadRequest) 110 | return 111 | } 112 | 113 | var responseJSON []byte 114 | var err error 115 | if h.Pretty { 116 | responseJSON, err = json.MarshalIndent(output, "", " ") 117 | } else { 118 | responseJSON, err = json.Marshal(output) 119 | } 120 | 121 | if err != nil { 122 | http.Error(w, err.Error(), http.StatusBadRequest) 123 | return 124 | } 125 | 126 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 127 | w.WriteHeader(http.StatusOK) 128 | w.Write(responseJSON) 129 | } 130 | 131 | func resolveJSON(body []byte) interface{} { 132 | if bytes.HasPrefix(body, []byte("[")) { 133 | var queries []*RequestOptions 134 | err := json.Unmarshal(body, &queries) 135 | if err != nil { 136 | // Probably `variables` was sent as a string instead of an object. 137 | // So, we try to be polite and try to parse that as a JSON string 138 | var optionsCompatible []*requestOptionsCompatibility 139 | json.Unmarshal(body, &optionsCompatible) 140 | for i := range optionsCompatible { 141 | json.Unmarshal([]byte(optionsCompatible[i].Variables), &queries[i].Variables) 142 | } 143 | return queries 144 | } 145 | return queries 146 | } 147 | 148 | var query RequestOptions 149 | err := json.Unmarshal(body, &query) 150 | if err != nil { 151 | // Probably `variables` was sent as a string instead of an object. 152 | // So, we try to be polite and try to parse that as a JSON string 153 | var optionsCompatible requestOptionsCompatibility 154 | json.Unmarshal(body, &optionsCompatible) 155 | json.Unmarshal([]byte(optionsCompatible.Variables), &query.Variables) 156 | return &query 157 | } 158 | return &query 159 | } 160 | --------------------------------------------------------------------------------