github.com/dgraph-io/dgraph@v1.2.8/graphql/web/http.go (about) 1 /* 2 * Copyright 2019 Dgraph Labs, Inc. and Contributors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package web 18 19 import ( 20 "compress/gzip" 21 "encoding/json" 22 "fmt" 23 "io" 24 "mime" 25 "net/http" 26 "strings" 27 28 "github.com/golang/glog" 29 "go.opencensus.io/trace" 30 31 "github.com/dgraph-io/dgraph/graphql/api" 32 "github.com/dgraph-io/dgraph/graphql/resolve" 33 "github.com/dgraph-io/dgraph/graphql/schema" 34 "github.com/dgraph-io/dgraph/x" 35 "github.com/pkg/errors" 36 ) 37 38 // An IServeGraphQL can serve a GraphQL endpoint (currently only ons http) 39 type IServeGraphQL interface { 40 41 // After ServeGQL is called, this IServeGraphQL serves the new resolvers. 42 ServeGQL(resolver *resolve.RequestResolver) 43 44 // HTTPHandler returns a http.Handler that serves GraphQL. 45 HTTPHandler() http.Handler 46 } 47 48 type graphqlHandler struct { 49 resolver *resolve.RequestResolver 50 handler http.Handler 51 } 52 53 // NewServer returns a new IServeGraphQL that can serve the given resolvers 54 func NewServer(resolver *resolve.RequestResolver) IServeGraphQL { 55 gh := &graphqlHandler{resolver: resolver} 56 gh.handler = api.WithRequestID(recoveryHandler(commonHeaders(gh))) 57 return gh 58 } 59 60 func (gh *graphqlHandler) HTTPHandler() http.Handler { 61 return gh.handler 62 } 63 64 func (gh *graphqlHandler) ServeGQL(resolver *resolve.RequestResolver) { 65 gh.resolver = resolver 66 } 67 68 // write chooses between the http response writer and gzip writer 69 // and sends the schema response using that. 70 func write(w http.ResponseWriter, rr *schema.Response, errMsg string, acceptGzip bool) { 71 var out io.Writer = w 72 73 // If the receiver accepts gzip, then we would update the writer 74 // and send gzipped content instead. 75 if acceptGzip { 76 w.Header().Set("Content-Encoding", "gzip") 77 gzw := gzip.NewWriter(w) 78 defer gzw.Close() 79 out = gzw 80 } 81 82 if _, err := rr.WriteTo(out); err != nil { 83 glog.Error(errMsg, err) 84 } 85 } 86 87 // ServeHTTP handles GraphQL queries and mutations that get resolved 88 // via GraphQL->Dgraph->GraphQL. It writes a valid GraphQL JSON response 89 // to w. 90 func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 91 92 ctx, span := trace.StartSpan(r.Context(), "handler") 93 defer span.End() 94 95 if !gh.isValid() { 96 panic("graphqlHandler not initialised") 97 } 98 99 var res *schema.Response 100 gqlReq, err := getRequest(r) 101 if err != nil { 102 res = schema.ErrorResponse(err, api.RequestID(ctx)) 103 } else { 104 res = gh.resolver.Resolve(ctx, gqlReq) 105 } 106 107 write(w, res, fmt.Sprintf("[%s]", api.RequestID(ctx)), 108 strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")) 109 110 } 111 112 func (gh *graphqlHandler) isValid() bool { 113 return !(gh == nil || gh.resolver == nil) 114 } 115 116 type gzreadCloser struct { 117 *gzip.Reader 118 io.Closer 119 } 120 121 func (gz gzreadCloser) Close() error { 122 err := gz.Reader.Close() 123 if err != nil { 124 return err 125 } 126 return gz.Closer.Close() 127 } 128 129 func getRequest(r *http.Request) (*schema.Request, error) { 130 gqlReq := &schema.Request{} 131 132 if r.Header.Get("Content-Encoding") == "gzip" { 133 zr, err := gzip.NewReader(r.Body) 134 if err != nil { 135 return nil, errors.Wrap(err, "Unable to parse gzip") 136 } 137 r.Body = gzreadCloser{zr, r.Body} 138 } 139 140 switch r.Method { 141 case http.MethodGet: 142 query := r.URL.Query() 143 gqlReq.Query = query.Get("query") 144 gqlReq.OperationName = query.Get("operationName") 145 variables, ok := query["variables"] 146 if ok { 147 d := json.NewDecoder(strings.NewReader(variables[0])) 148 d.UseNumber() 149 150 if err := d.Decode(&gqlReq.Variables); err != nil { 151 return nil, errors.Wrap(err, "Not a valid GraphQL request body") 152 } 153 } 154 case http.MethodPost: 155 mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 156 if err != nil { 157 return nil, errors.Wrap(err, "unable to parse media type") 158 } 159 160 switch mediaType { 161 case "application/json": 162 d := json.NewDecoder(r.Body) 163 d.UseNumber() 164 if err = d.Decode(&gqlReq); err != nil { 165 return nil, errors.Wrap(err, "Not a valid GraphQL request body") 166 } 167 default: 168 // https://graphql.org/learn/serving-over-http/#post-request says: 169 // "A standard GraphQL POST request should use the application/json 170 // content type ..." 171 return nil, errors.New( 172 "Unrecognised Content-Type. Please use application/json for GraphQL requests") 173 } 174 default: 175 return nil, 176 errors.New("Unrecognised request method. Please use GET or POST for GraphQL requests") 177 } 178 179 return gqlReq, nil 180 } 181 182 func commonHeaders(next http.Handler) http.Handler { 183 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 184 x.AddCorsHeaders(w) 185 w.Header().Set("Content-Type", "application/json") 186 187 next.ServeHTTP(w, r) 188 }) 189 } 190 191 func recoveryHandler(next http.Handler) http.Handler { 192 193 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 194 reqID := api.RequestID(r.Context()) 195 defer api.PanicHandler(reqID, 196 func(err error) { 197 rr := schema.ErrorResponse(err, reqID) 198 write(w, rr, fmt.Sprintf("[%s]", reqID), 199 strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")) 200 }) 201 202 next.ServeHTTP(w, r) 203 }) 204 }