graphql icon indicating copy to clipboard operation
graphql copied to clipboard

header access

Open PianoOrDota2 opened this issue 6 years ago • 17 comments

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

PianoOrDota2 avatar Aug 10 '18 10:08 PianoOrDota2

Is it possible currently to access headers?

kamushadenes avatar Mar 22 '19 15:03 kamushadenes

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.

chris-ramon avatar Mar 22 '19 16:03 chris-ramon

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?

kamushadenes avatar Mar 22 '19 16:03 kamushadenes

@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.

kamushadenes avatar Mar 22 '19 16:03 kamushadenes

@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.

kamushadenes avatar Mar 25 '19 06:03 kamushadenes

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()

	})
}

kamushadenes avatar Mar 25 '19 06:03 kamushadenes

The other way is to create a dictionary of headers then pass it to context using context.WithValue(context.Background(),...

ghost avatar Mar 25 '19 15:03 ghost

I also have the same doubts. I need to get the request header in the Resolve method. Is there a way?

kingzcheung avatar Apr 19 '19 08:04 kingzcheung

Of course. Check what I said, you need to pass the parameters using context.withValue

ghost avatar Apr 19 '19 13:04 ghost

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 avatar May 08 '19 09:05 harshithjv

@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)
}

david-castaneda avatar Jun 06 '19 05:06 david-castaneda

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
}

jschmidtnj avatar Dec 26 '19 06:12 jschmidtnj

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
            },
        },
    },
}

bmdelacruz avatar Feb 25 '20 09:02 bmdelacruz

@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. :)

harshithjv avatar Jul 30 '20 08:07 harshithjv

@kamushadenes Where can I find implementation of qhttp.NewRewritableResponseWriter(w)?

mimol91 avatar Jun 25 '21 06:06 mimol91

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)

}

18580764418 avatar Apr 29 '22 07:04 18580764418

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}
		

gcsfred2 avatar Oct 28 '22 17:10 gcsfred2