graphql
graphql copied to clipboard
header access
shall we add the header access from the request and the response, the change will pass along the call stack to the resolver function, but it will be useful
Is it possible currently to access headers?
Is it possible currently to access headers?
Hi @kamushadenes, thanks for using the lib and reaching us for a question, you can access request headers as you would normally do via the net/http.Header.Get
:
(graphql)-> go doc http.Header.Get
func (h Header) Get(key string) string
Get gets the first value associated with the given key. It is case
insensitive; textproto.CanonicalMIMEHeaderKey is used to canonicalize the
provided key. If there are no values associated with the key, Get returns
"". To access multiple values of a key, or to use non-canonical keys, access
the map directly.
Hello @chris-ramon, thank you for your response.
It's not clear to me how to access the http request though the Resolve method (p ResolveParams). Can you please give me an example?
An alternative I thought of is using a middleware that takes the relevant headers and passes them by using the context.
If both ways (direct acessing the http request X using a middleware) are possible, which do you think is more elegant?
@chris-ramon can you also point me to how I can access the original request body?
I need to make a passthrough to another system after collecting some metrics, so I need the original body to replay the query.
@chris-ramon I ended up coding a middleware that fetches the relevant headers and request body and puts them into the context (as seem in #269).
I also made a custom writer that delays WriteHeader() calls so that I can add some late headers that are only available after everything is processed by the handler.
I would love a better way but I couldn't come with one.
func StargateMiddleware(next http.Handler) http.Handler {
ctx := context.WithValue(context.Background(), "requestUUID", utils.GetUUID().String())
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
r.Body = ioutil.NopCloser(bytes.NewReader(b))
ctx = context.WithValue(ctx, "originalRequest", string(b))
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = strings.Split(r.RemoteAddr, ":")[0]
}
ctx = context.WithValue(ctx, "remoteAddress", ip)
ctx = context.WithValue(ctx, "partnerUUID", "TODO")
writer := qhttp.NewRewritableResponseWriter(w)
next.ServeHTTP(writer, r.WithContext(ctx))
writer.Header()["RequestUUID"] = []string{ctx.Value("requestUUID").(string)}
writer.Commit()
})
}
The other way is to create a dictionary of headers then pass it to context using context.WithValue(context.Background(),...
I also have the same doubts. I need to get the request header in the Resolve method. Is there a way?
Of course. Check what I said, you need to pass the parameters using context.withValue
Where do you write this Middleware? Where is context variable available to middleware function?
I am using graphql-go within Gin-Gonic.
On graphql.go code:
...
import (
"github.com/gin-gonic/gin"
"github.com/graphql-go/graphql"
"github.com/graphql-go/handler"
)
...
// Handler initializes the prometheus middleware.
func GraphQLHandler() gin.HandlerFunc {
// Creates a GraphQL-go HTTP handler with the defined schema
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: false,
Playground: false,
})
return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}
On main.go :
...
func main() {
router = gin.Default()
router.Use(...)
...
graphql := router.Group("/graphql")
graphql.POST("", handlers.GraphQLHandler())
}
So where should I write the middleware in this case?
@harshithjv what i ended up doing is the following
Create a middleware function to extract the headers:
func httpHeaderMiddleware(next *handler.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "header", r.Header)
next.ContextHandler(ctx, w, r)
})
}
wrap my handler in this function:
func GQLHandler(schema graphql.Schema) *handler.Handler {
handler := handler.New(
&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: false,
Playground: true,
})
return httpHeaderMiddleware(handler)
}
this is what I ended up doing:
func graphqlMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// before request
// set auth
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), tokenKey, getAuthToken(c.Request)))
c.Next()
// after request
}
}
then:
accountdata, err := validateLoggedIn(params.Context.Value(tokenKey).(string))
if err != nil {
return nil, err
}
You can provide a RootObjectFn
to the handler.Config
struct of github.com/graphql-go/handler
package. Its function signature is type RootObjectFn func(ctx context.Context, r *http.Request) map[string]interface{}
. The value you return from that function is equal to the value of p.Info.RootValue
where p
is a graphql.ResolveParams
object.
Reference pull request: https://github.com/graphql-go/handler/pull/42
Example:
// setup the graphql handler
graphqlHandler := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: false,
Playground: true,
RootObjectFn: func(ctx context.Context, r *http.Request) map[string]interface{} {
return map[string]interface{}{
"hello": "world",
}
},
})
// setup the root query
rootQuery := graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"hello": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
fmt.Println(p.Info.RootValue) // prints: map[hello:world]
return "woot!", nil
},
},
},
}
@david-castaneda @jschmidtnj @bmdelacruz
Thanks all for the suggestion and your time guys. I hadn't worked for much time on Go-lang or GraphQL and was working on a short time POC. But it was abruptly ended for another project requirement and therefore I hadn't seen your comments. So right now I can't test these suggestions. However thanks for your efforts and hope it help others who read this thread. :)
@kamushadenes
Where can I find implementation of qhttp.NewRewritableResponseWriter(w)
?
package main
import ( "bytes" "context" "encoding/json" "fmt" "github.com/pkg/errors" "io" "log" "mime/multipart" "net/http" "strings" )
// Package graphql provides a low level GraphQL client.
//
// // create a client (safe to share across requests)
// client := graphql.NewClient("https://machinebox.io/graphql")
//
// // make a request
// req := graphql.NewRequest(// query ($key: String!) { // items (id:$key) { // field1 // field2 // field3 // } // } //
)
//
// // set any variables
// req.Var("key", "value")
//
// // run it and capture the response
// var respData ResponseStruct
// if err := client.Run(ctx, req, &respData); err != nil {
// log.Fatal(err)
// }
//
// Specify client
//
// To specify your own http.Client, use the WithHTTPClient option:
// httpclient := &http.Client{}
// client := graphql.NewClient("https://machinebox.io/graphql", graphql.WithHTTPClient(httpclient))
//package graphql
//
//import (
//"bytes"
//"context"
//"encoding/json"
//"fmt"
//"io"
//"mime/multipart"
//"net/http"
//
//"github.com/pkg/errors"
//)
// Client is a client for interacting with a GraphQL API. type Client struct { endpoint string httpClient *http.Client useMultipartForm bool
// Log is called with various debug information.
// To log to standard out, use:
// client.Log = func(s string) { log.Println(s) }
Log func(s string)
}
// NewClient makes a new Client capable of making GraphQL requests. func NewClient(endpoint string, opts ...ClientOption) *Client { c := &Client{ endpoint: endpoint, Log: func(string) {}, } for _, optionFunc := range opts { optionFunc(c) } if c.httpClient == nil { c.httpClient = http.DefaultClient } return c }
func (c *Client) logf(format string, args ...interface{}) { c.Log(fmt.Sprintf(format, args...)) }
// Run executes the query and unmarshals the response from the data field // into the response object. // Pass in a nil response object to skip response parsing. // If the request fails or the server returns an error, the first error // will be returned. func (c *Client) Run(ctx context.Context, req *Request, resp *graphResponse) error { select { case <-ctx.Done(): return ctx.Err() default: } if len(req.files) > 0 && !c.useMultipartForm { return errors.New("cannot send files with PostFields option") } if c.useMultipartForm { return c.runWithPostFields(ctx, req, resp) } return c.runWithJSON(ctx, req, resp) }
func (c *Client) runWithJSON(ctx context.Context, req *Request, gr *graphResponse) error {
var requestBody bytes.Buffer
requestBodyObj := struct {
Query string json:"query"
Variables map[string]interface{} json:"variables"
}{
Query: req.q,
Variables: req.vars,
}
if err := json.NewEncoder(&requestBody).Encode(requestBodyObj); err != nil {
return errors.Wrap(err, "encode body")
}
c.logf(">> variables: %v", req.vars)
c.logf(">> query: %s", req.q)
r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody)
if err != nil {
return err
}
r.Header.Set("Content-Type", "application/json; charset=utf-8")
r.Header.Set("Accept", "application/json; charset=utf-8")
for key, values := range req.Header {
for _, value := range values {
r.Header.Add(key, value)
}
}
c.logf(">> headers: %v", r.Header)
r = r.WithContext(ctx)
res, err := c.httpClient.Do(r)
if err != nil {
return err
}
defer res.Body.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, res.Body); err != nil {
return errors.Wrap(err, "reading body")
}
c.logf("<< %s", buf.String())
if err := json.NewDecoder(&buf).Decode(&gr); err != nil {
return errors.Wrap(err, "decoding response")
}
gr.Header = res.Header
if len(gr.Errors) > 0 {
// return first error
return gr.Errors[0]
}
return nil
}
func (c *Client) runWithPostFields(ctx context.Context, req *Request, gr *graphResponse) error { var requestBody bytes.Buffer writer := multipart.NewWriter(&requestBody) if err := writer.WriteField("query", req.q); err != nil { return errors.Wrap(err, "write query field") } var variablesBuf bytes.Buffer if len(req.vars) > 0 { variablesField, err := writer.CreateFormField("variables") if err != nil { return errors.Wrap(err, "create variables field") } if err := json.NewEncoder(io.MultiWriter(variablesField, &variablesBuf)).Encode(req.vars); err != nil { return errors.Wrap(err, "encode variables") } } for i := range req.files { part, err := writer.CreateFormFile(req.files[i].Field, req.files[i].Name) if err != nil { return errors.Wrap(err, "create form file") } if _, err := io.Copy(part, req.files[i].R); err != nil { return errors.Wrap(err, "preparing file") } } if err := writer.Close(); err != nil { return errors.Wrap(err, "close writer") } c.logf(">> variables: %s", variablesBuf.String()) c.logf(">> files: %d", len(req.files)) c.logf(">> query: %s", req.q)
r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody)
if err != nil {
return err
}
r.Header.Set("Content-Type", writer.FormDataContentType())
r.Header.Set("Accept", "application/json; charset=utf-8")
for key, values := range req.Header {
for _, value := range values {
r.Header.Add(key, value)
}
}
c.logf(">> headers: %v", r.Header)
r = r.WithContext(ctx)
res, err := c.httpClient.Do(r)
if err != nil {
return err
}
defer res.Body.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, res.Body); err != nil {
return errors.Wrap(err, "reading body")
}
c.logf("<< %s", buf.String())
if err := json.NewDecoder(&buf).Decode(&gr); err != nil {
return errors.Wrap(err, "decoding response")
}
gr.Header = res.Header
if len(gr.Errors) > 0 {
// return first error
return gr.Errors[0]
}
return nil
}
// WithHTTPClient specifies the underlying http.Client to use when // making requests. // NewClient(endpoint, WithHTTPClient(specificHTTPClient)) func WithHTTPClient(httpclient *http.Client) ClientOption { return func(client *Client) { client.httpClient = httpclient } }
// UseMultipartForm uses multipart/form-data and activates support for // files. func UseMultipartForm() ClientOption { return func(client *Client) { client.useMultipartForm = true } }
// ClientOption are functions that are passed into NewClient to // modify the behaviour of the Client. type ClientOption func(*Client)
type graphErr struct { Message string }
func (e graphErr) Error() string { return "graphql: " + e.Message }
type graphResponse struct { Data interface{} Header http.Header Errors []graphErr }
// Request is a GraphQL request. type Request struct { q string vars map[string]interface{} files []file
// Header represent any request headers that will be set
// when the request is made.
Header http.Header
}
// NewRequest makes a new Request with the specified string. func NewRequest(q string) *Request { req := &Request{ q: q, Header: make(map[string][]string), } return req }
// Var sets a variable. func (req *Request) Var(key string, value interface{}) { if req.vars == nil { req.vars = make(map[string]interface{}) } req.vars[key] = value }
// File sets a file to upload. // Files are only supported with a Client that was created with // the UseMultipartForm option. func (req *Request) File(fieldname, filename string, r io.Reader) { req.files = append(req.files, file{ Field: fieldname, Name: filename, R: r, }) }
// file represents a file to upload. type file struct { Field string Name string R io.Reader }
func graphQlMain(){
httpClient := &http.Client{}
client := NewClient("http://192.168.2.121/graphql", WithHTTPClient(httpClient))
req := NewRequest(mutation ($username: String!, $password: String!) { auth { login(username: $username, password: $password) { ... on Error { message __typename } __typename } __typename } }
)
req.Var("username","root")
req.Var("password", "root")
req.Header.Set("Cache-Control", "no-cache")
ctx := context.Background()
var respData graphResponse
if err := client.Run(ctx, req, &respData); err != nil {
log.Fatal(err)
}
log.Print(respData)
cookieResp := respData.Header.Get("Set-Cookie")
cookie := strings.Split(cookieResp, ";")[0]
req1 := NewRequest(`
mutation ($tuneInput: AutotuningIn!, $apply: Boolean!) {
bosminer {
config {
updateAutotuning(input: $tuneInput, apply: $apply) {
... on AttributeError {
message
__typename
}
... on AutotuningError {
message
powerScaling {
powerStep
shutdownDuration
minPsuPowerLimit
__typename
}
psuPowerLimit
__typename
}
... on AutotuningOut {
__typename
}
__typename
}
__typename
}
__typename
}
}
`)
power := struct {
PsuPowerLimit int `json:"psuPowerLimit"`
}{
5000,
}
req1.Var("tuneInput", power)
req1.Var("apply", false)
req1.Header.Set("Cache-Control", "no-cache")
req1.Header.Set("Cookie", cookie)
var respData1 graphResponse
if err := client.Run(ctx, req1, &respData1); err != nil {
log.Fatal(err)
}
log.Print(respData1)
}
In my case:
type TTransport struct {
rt http.RoundTripper
}
func (t *TTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Add("authorization", "Bearer <etc>")
return t.rt.RoundTrip(req)
}
...
client := http.DefaultClient
client.Transport = &TTransport{http.DefaultTransport}