Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass dynamic header to federated services #383

Closed
mxncson opened this issue Jul 6, 2022 · 11 comments
Closed

Pass dynamic header to federated services #383

mxncson opened this issue Jul 6, 2022 · 11 comments
Labels
internally-reviewed Internally reviewed

Comments

@mxncson
Copy link

mxncson commented Jul 6, 2022

Hello @jensneuse,

How do you pass a dynamic header to the http handler (engine) ?
I have a simple use case where the client send an auth id that i need to validate(gateway side) then forward others different headers to my services.

I don't see a direct way to interface with the underlying http components in the engine. How can i pass a custom and non-generic headers to services ?

I've read #270 and the other unanswered issue similar to mine, i have tested all of the above and nothing fit this case.

@jensneuse
Copy link
Member

In WunderGraph, we do it this way, works also for Federation: https://wundergraph.com/docs/guides/strategies/inject_short_lived_token_to_upstream_requests
That's OSS too, just a level of abstraction to make things easy.
If you want to dig deeper yourself, here's a pointer on how to access headers during execution:

Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$1$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!){droid(id: $id){name aliased: name friends {name} primaryFunction} hero {name} stringList nestedStringList}","variables":{"id":$$0$$}}}`,

Client Request Headers need to be passed to the resolver as part of the execution context to be able to use them during resolving:

@aerfio
Copy link

aerfio commented Aug 15, 2022

@jensneuse
For me it's not working, I've debugged it quite a bit
Following an examples/federation, you can check that it's broken by applying following patch:

diff --git a/examples/federation/gateway/http/http.go b/examples/federation/gateway/http/http.go
index a29a4091..a90907ad 100644
--- a/examples/federation/gateway/http/http.go
+++ b/examples/federation/gateway/http/http.go
@@ -27,7 +27,9 @@ func (g *GraphQLHTTPRequestHandler) handleHTTP(w http.ResponseWriter, r *http.Re
 
 	buf := bytes.NewBuffer(make([]byte, 0, 4096))
 	resultWriter := graphql.NewEngineResultWriterFromBuffer(buf)
-	if err = g.engine.Execute(r.Context(), &gqlRequest, &resultWriter); err != nil {
+	if err = g.engine.Execute(r.Context(), &gqlRequest, &resultWriter, graphql.WithAdditionalHttpHeaders(http.Header{
+		"X-Request-ID": []string{"123456"},
+	})); err != nil {
 		g.log.Error("engine.Execute", log.Error(err))
 		w.WriteHeader(http.StatusInternalServerError)
 		return
diff --git a/examples/federation/products/server.go b/examples/federation/products/server.go
index 1cf18943..46a3ca7e 100644
--- a/examples/federation/products/server.go
+++ b/examples/federation/products/server.go
@@ -20,7 +20,10 @@ func main() {
 	}
 
 	http.Handle("/", playground.Handler("GraphQL playground", "/query"))
-	http.Handle("/query", graph.GraphQLEndpointHandler(graph.EndpointOptions{EnableDebug: true, EnableRandomness: true}))
+	http.Handle("/query", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		log.Println(r.Header)
+		graph.GraphQLEndpointHandler(graph.EndpointOptions{EnableDebug: true, EnableRandomness: true}).ServeHTTP(w, r)
+	}))
 	http.HandleFunc("/websocket_connections", graph.WebsocketConnectionsHandler)
 
 	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)

treat X-Request-ID by any user defined header.

If I debugged correctly it's because

func (e *ExecutionEngineV2) getCachedPlan(ctx *internalExecutionContext, operation, definition *ast.Document, operationName string, report *operationreport.Report) plan.Plan {

does not take into account any header that is in ctx.Request.Header.

@genesor
Copy link

genesor commented Aug 18, 2022

@aerfio there is another step required to have the HTTP Headers passed to the underlying services for the federated gateway.

When creating your schema configuration you need to add the header with a custom syntax that will look for a value in the original HTTP request.

serviceCfg := graphqlSchema.Configuration{
	Fetch: graphqlSchema.FetchConfiguration{
		URL:    url,
		Method: http.MethodPost,
		Header: http.Header{
			// .request.headers.XXX transmits incoming HTTP headers value to the
			// internal sub-requests made
			"X-Request-ID":          []string{"{{ .request.headers.X-Request-ID }}"},
		},
	},
	Federation: graphqlSchema.FederationConfiguration{
		Enabled:    true,
		ServiceSDL: sdl,
	},
}

@reistiago
Copy link

Should a similar config work for subscriptions?

I'm trying to propagate headers in the init message by doing something similar to what is here, but the options.Header is always empty.

I've seen this commit, that just landed in master, which does it in a different way, but similar in the end, but I should still have the problem of the headers being empty.

@BenjaminYong
Copy link
Contributor

Bump! Same as @reistiago , I am having difficulty propagating the headers from my subscription request to the federated subgraphs. It works fine for queries and mutations.

Here's my data source config:

dataSourceConfig := graphqlDataSource.Configuration{
	Fetch: graphqlDataSource.FetchConfiguration{
		URL:    serviceConfig.URL,
		Method: http.MethodPost,
		Header: http.Header{
			"Authorization": []string{"{{ .request.headers.Authorization }}"},
		},
	},
	Subscription: graphqlDataSource.SubscriptionConfiguration{
		URL: serviceConfig.WS,
	},
	Federation: graphqlDataSource.FederationConfiguration{
		Enabled:    true,
		ServiceSDL: sdl,
	},
}

I've debugged in my func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) HTTP handler and can print the Authorization header in the request. It seems to be lost downstream or not sent to the subgraphs.

@StarpTech StarpTech added the internally-reviewed Internally reviewed label Mar 11, 2024
@IsabelFreitas-catapult
Copy link

IsabelFreitas-catapult commented May 17, 2024

Any updates on this? I am using github.com/wundergraph/graphql-go-tools v1.67.2 and the initialPayload is not being propagated to the subgraphs
@reistiago @BenjaminYong did you get this to work with subscriptions?
@genesor

@waeljammal
Copy link

This is a major issue for us as well, using the example code we are unable to pass custom headers to our code when using WithAdditionalHttpHeaders or otherwise.

@IsabelFreitas-catapult
Copy link

IsabelFreitas-catapult commented Jun 18, 2024

Ok I managed to propagate the headers to a subscription by doing the following.
in ws.go I updated HandleWebsocket to use the latest handler

import (
	"github.com/wundergraph/graphql-go-tools/pkg/subscription/websocket"
)

func HandleWebsocket(done chan bool, errChan chan error, conn net.Conn,
	executorPool *subscription.ExecutorV2Pool, logger *slog.Logger) {
	defer func() {
		if err := conn.Close(); err != nil {
			logger.Error("http.HandleWebsocket(): could not close connection to client.", err)
		}
	}()

	transportProtocol := websocket.Protocol(helper.GetEnv(configuration.EnvTransportProtocol,
		string(websocket.ProtocolGraphQLTransportWS)))

	websocket.Handle(
		done,
		errChan,
		conn,
		executorPool,
		websocket.WithProtocol(transportProtocol),
		websocket.WithCustomSubscriptionUpdateInterval(50*time.Millisecond),
		websocket.WithCustomKeepAliveInterval(graph_server.WebsocketKeepAliveInterval),
	)
}

WithAdditionalHttpHeaders only gets called if the request is of type InitialHttpRequestContext so I updated the code to convert the request context to type InitialHttpRequestContext (see var newContext bellow) which then allows WithAdditionalHttpHeaders to be called.
So handleWebsocket in ws.go was updated to this:

import (
	"encoding/json"
	"fmt"
	"github.com/wundergraph/graphql-go-tools/pkg/subscription/websocket"
	"net/http"
	"time"

	"github.com/gobwas/ws"
	"github.com/gobwas/ws/wsutil"
	"github.com/wundergraph/graphql-go-tools/pkg/subscription"
	"log/slog"
	"net"
)

// handleWebsocket will handle the websocket connection.
func (g *GraphQLHTTPRequestHandler) handleWebsocket(req http.Request, conn net.Conn) {
	done := make(chan bool)
	errChan := make(chan error)

	newContext := subscription.NewInitialHttpRequestContext(&req)
	executorPool := subscription.NewExecutorV2Pool(g.engine, newContext)

	go HandleWebsocket(done, errChan, conn, executorPool, g.log)
	select {
	case err := <-errChan:
		g.log.Error("http.GraphQLHTTPRequestHandler.handleWebsocket()", err)
	case <-done:
	}
}

This also meant that I had to update the upgradeWithNewGoroutine in handler.go to:

func (g *GraphQLHTTPRequestHandler) upgradeWithNewGoroutine(w http.ResponseWriter, r *http.Request) error {
	conn, _, _, err := g.wsUpgrader.Upgrade(r, w)
	if err != nil {
		return err
	}

	g.handleWebsocket(*r, conn)
	return nil
}

previously only the context was being sent to the handleWebsocket and now it requires the whole request to be sent through.

my datasource_poller.go looks like this

func (d *DatasourcePollerPoller) createDatasourceConfig() []graphqlDataSource.Configuration {
	dataSourceConfigs := make([]graphqlDataSource.Configuration, 0, len(d.config.Services))

	for _, serviceConfig := range d.config.Services {
		sdl, exists := d.sdlMap[serviceConfig.Name]
		if !exists {
			continue
		}

		dataSourceConfig := graphqlDataSource.Configuration{
			Fetch: graphqlDataSource.FetchConfiguration{
				URL:    serviceConfig.URL,
				Method: http.MethodPost,
				Header: http.Header{
					// .request.headers.XXX transmits incoming HTTP headers value to the
					// internal sub-requests made
					"X-Customer-Id":   []string{"{{ .request.headers.X-Customer-Id }}"},
					"X-SOB":   []string{"{{ .request.headers.X-SOB }}"},
				},
			},
			Subscription: graphqlDataSource.SubscriptionConfiguration{
				URL: serviceConfig.WS,
			},
			Federation: graphqlDataSource.FederationConfiguration{
				Enabled:    true,
				ServiceSDL: sdl,
			},
		}

		dataSourceConfigs = append(dataSourceConfigs, dataSourceConfig)
	}

	return dataSourceConfigs
}

@waeljammal I hope this helps

@besmiralia
Copy link

@mxncson were you able to find a solution to your request?
I am facing the same issue.
I want to replace the initial Auth header with a delegated header based on the downstream service being called.
I have followed the demo from examples folder. I see the datasource_poller file creates a list of SubgraphConfiguration which has only Name, URL, SDL and SubscriptionUrl. However, based on the examples mentioned here, I see a different type graphqlDataSource.Configuration.

Ultimately, I would like to hook into the fetch request before it is sent to the downstream service to overwrite it's headers.
Thank you

@justgook
Copy link

i just want to pass cookies / auth data / referrer down to service, and still don't see how it can be done int v2 version..

@jensneuse
Copy link
Member

In Cosmo Router, we add the client request Header to the context and forward it to Subgraphs in the transport like so:

https://github.com/wundergraph/cosmo/blob/d79e08aec891655b206749a90abfd4fb371c78cd/router/core/graphql_handler.go#L159

https://github.com/wundergraph/cosmo/blob/d79e08aec891655b206749a90abfd4fb371c78cd/router/core/header_rule_engine.go#L102

Cosmo Router has a Header Rule Engine to propagate Headers dynamically, with defaults and such. You can replicate this.
Please re-open if there are still questions left.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
internally-reviewed Internally reviewed
Projects
None yet
Development

No branches or pull requests