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  }