Repository: Khan/genqlient
Pull request #186 (2022-04-08 13:08:51)

Add support for client that uses GET as transport mechanism
Current implementation always uses POST as the transport mechanism. Adding GET support enables usage of GET queries for caching simply via URL.

Some notes:
- I left the existing API for creating a new client as is, but the implementation could be much cleaner by introducing some sort of configuration struct when creating a new client
- The construction of the query parameters follows the logic from Apollo's client implementation, which can be found here https://github.com/apollographql/apollo-client/blob/8beb4820edc6352996e08f7f73bde3573f1eb666/src/link/http/rewriteURIForGET.ts
- Updated integration tests to use both sets of clients. Updating the tests to use a test suite would be cleaner

I have:
- [x] Written a clear PR title and description (above)
- [x] Signed the [Khan Academy CLA](https://www.khanacademy.org/r/cla)
- [x] Added tests covering my changes, if applicable
- [x] Included a link to the issue fixed, if applicable
- [x] Included documentation, for new features
- [x] Added an entry to the changelog


salman-rb: > The code looks right to me!, though I'm not one of the genqlient experts. Your changes are clear and make a lot of sense.
> 
> I think the main issue here is more philosophical, about whether we want to expose this at all. It's really easy for variables to include personal information, such as passwords, and exposing those via GET -- where they're logged, if nothing else -- is potentially problematic. There are certainly legit uses for graphql over GET, but I'm worried this introduces an easy-to-use footgun. But arguably it's not our job as library authors to dictate that kind of thing.

Definitely agree with your concerns. I believe the mechanism should be included given that its part of the graphql specification and there are good use cases for it. Maybe there is an opportunity to make it more specific at request time that you are using GET as the transport mechanism to avoid the foot gun, as where the client is first created might be different than where it gets used. I believe this would require changing some of the public APIs to include the request method, will wait for more feedback before making a change like that
benjaminjkraft: Thanks.  I think this makes sense to include; while users can bring their own client this one is probably common enough it's worth implementing it once, correctly, so everyone can use.

One question is whether it's a good idea to allow GET to be used for mutations.  I would think if you're doing that you're really in for trouble, whether as Craig says via logging your passwords, or simply in something not knowing the request isn't idempotent.  If someone really wants the foot-gun, they can just copy the implementation and remove the check.  Since the client has access to the query you can implement this by just checking if the first word (after whitespace) is "query" or "mutation" (I believe genqlient will never put a comment before the query it sends to the server, although that's perhaps slightly fragile; if it's ever an issue we could give the client the operation type or even the entire parsed query).  We'd want to mention that in the documentation -- actually it's probably a good idea as a recommendation even if we don't require it.

FWIW I actually like having two separate client constructors, as it is now.  It's possible it will become unwieldy, but I feel they're actually separate methods of serializing GraphQL over HTTP, not just two different request methods.

Lastly, I think you still need to sign the CLA?  Or at least, you didn't check the checkbox :-) .
benjaminjkraft: Great! Looks good to merge as soon as the CLA is sorted, let us know if you need anything from us on that!
salman-rb: > Great! Looks good to merge as soon as the CLA is sorted, let us know if you need anything from us on that!

Should be sorted! Let me know if there are any issues 

Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index a86884e..6e0bd6b 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -29,6 +29,7 @@ When releasing a new version:
 
 - genqlient can now run as a portable binary (i.e. without a local checkout of the repository or `go run`).
 - You can now enable `use_extensions` in the configuration file, to receive extensions returned by the GraphQL API server. Generated functions will return extensions as `map[string]interface{}`, if enabled.
+- You can now use `graphql.NewClientUsingGet` to create a client that uses query parameters to pass the query to the GraphQL API server.
 
 ### Bug fixes:
 
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 496fcb2..4793b4f 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -8,6 +8,34 @@ This document describes common questions about genqlient, and provides an index
 
 There's a [doc for that](INTRODUCTION.md)!
 
+##  … use GET requests instead of POST requests?
+
+You can use `graphql.NewClientUsingGet` to create a client that will use query parameters to create the request. For example:
+```go
+ctx := context.Background()
+client := graphql.NewClientUsingGet("https://api.github.com/graphql", http.DefaultClient)
+resp, err := getUser(ctx, client, "benjaminjkraft")
+fmt.Println(resp.User.Name, err)
+```
+
+This request will be sent via an HTTP GET request, with the query, operation name and variables encoded in the URL.
+
+For example, if the query is defined as:
+
+```graphql
+query getUser($login: String!) {
+  user(login: $login) {
+    name
+  }
+}
+```
+
+The URL requested will be:
+
+`https://api.github.com/graphql?operationName%3DgetUser%26query%3D%0Aquery%20getUser(%24login%3A%20String!)%20%7B%0A%20%20user(login%3A%20%24login)%20%7B%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A%26variables%3D%7B%22login%22%3A%22benjaminjkraft%22%7D`
+
+The client does not support mutations, and will return an error if passed a request that attempts one.
+
 ### … use an API that requires authentication?
 
 When you call `graphql.NewClient`, pass in an HTTP client that adds whatever authentication headers you need (typically by wrapping the client's `Transport`).  For example:
diff --git a/graphql/client.go b/graphql/client.go
index 4715470..600f243 100644
--- a/graphql/client.go
+++ b/graphql/client.go
@@ -4,9 +4,12 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
+	"net/url"
+	"strings"
 
 	"github.com/vektah/gqlparser/v2/gqlerror"
 )
@@ -53,10 +56,30 @@ type client struct {
 // The typical method of adding authentication headers is to wrap the client's
 // Transport to add those headers.  See example/caller.go for an example.
 func NewClient(endpoint string, httpClient Doer) Client {
+	return newClient(endpoint, httpClient, http.MethodPost)
+}
+
+// NewClientUsingGet returns a Client which makes requests to the given endpoint,
+// suitable for most users.
+//
+// The client makes GET requests to the given GraphQL endpoint using a GET query,
+// with the query, operation name and variables encoded as URL parameters.
+// It will use the given http client, or http.DefaultClient if a nil client is passed.
+//
+// The client does not support mutations, and will return an error if passed a request
+// that attempts one.
+//
+// The typical method of adding authentication headers is to wrap the client's
+// Transport to add those headers.  See example/caller.go for an example.
+func NewClientUsingGet(endpoint string, httpClient Doer) Client {
+	return newClient(endpoint, httpClient, http.MethodGet)
+}
+
+func newClient(endpoint string, httpClient Doer, method string) Client {
 	if httpClient == nil || httpClient == (*http.Client)(nil) {
 		httpClient = http.DefaultClient
 	}
-	return &client{httpClient, endpoint, http.MethodPost}
+	return &client{httpClient, endpoint, method}
 }
 
 // Doer encapsulates the methods from *http.Client needed by Client.
@@ -100,15 +123,14 @@ type Response struct {
 }
 
 func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response) error {
-	body, err := json.Marshal(req)
-	if err != nil {
-		return err
+	var httpReq *http.Request
+	var err error
+	if c.method == http.MethodGet {
+		httpReq, err = c.createGetRequest(req)
+	} else {
+		httpReq, err = c.createPostRequest(req)
 	}
 
-	httpReq, err := http.NewRequest(
-		c.method,
-		c.endpoint,
-		bytes.NewReader(body))
 	if err != nil {
 		return err
 	}
@@ -142,3 +164,66 @@ func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response)
 	}
 	return nil
 }
+
+func (c *client) createPostRequest(req *Request) (*http.Request, error) {
+	body, err := json.Marshal(req)
+	if err != nil {
+		return nil, err
+	}
+
+	httpReq, err := http.NewRequest(
+		c.method,
+		c.endpoint,
+		bytes.NewReader(body))
+	if err != nil {
+		return nil, err
+	}
+
+	return httpReq, nil
+}
+
+func (c *client) createGetRequest(req *Request) (*http.Request, error) {
+	parsedURL, err := url.Parse(c.endpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	queryParams := parsedURL.Query()
+	queryUpdated := false
+
+	if req.Query != "" {
+		if strings.HasPrefix(strings.TrimSpace(req.Query), "mutation") {
+			return nil, errors.New("client does not support mutations")
+		}
+		queryParams.Set("query", req.Query)
+		queryUpdated = true
+	}
+
+	if req.OpName != "" {
+		queryParams.Set("operationName", req.OpName)
+		queryUpdated = true
+	}
+
+	if req.Variables != nil {
+		variables, variablesErr := json.Marshal(req.Variables)
+		if variablesErr != nil {
+			return nil, variablesErr
+		}
+		queryParams.Set("variables", string(variables))
+		queryUpdated = true
+	}
+
+	if queryUpdated {
+		parsedURL.RawQuery = queryParams.Encode()
+	}
+
+	httpReq, err := http.NewRequest(
+		c.method,
+		parsedURL.String(),
+		http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+
+	return httpReq, nil
+}
diff --git a/internal/integration/generated.go b/internal/integration/generated.go
index 8ebe975..77d4268 100644
--- a/internal/integration/generated.go
+++ b/internal/integration/generated.go
@@ -598,6 +598,13 @@ type MoreUserFieldsHair struct {
 // GetColor returns MoreUserFieldsHair.Color, and is useful for accessing the field via an interface.
 func (v *MoreUserFieldsHair) GetColor() string { return v.Color }
 
+type NewUser struct {
+	Name string `json:"name"`
+}
+
+// GetName returns NewUser.Name, and is useful for accessing the field via an interface.
+func (v *NewUser) GetName() string { return v.Name }
+
 // QueryFragment includes the GraphQL fields of Query requested by the fragment QueryFragment.
 type QueryFragment struct {
 	Beings []QueryFragmentBeingsBeing `json:"-"`
@@ -991,6 +998,14 @@ func (v *UserFields) __premarshalJSON() (*__premarshalUserFields, error) {
 	return &retval, nil
 }
 
+// __createUserInput is used internally by genqlient
+type __createUserInput struct {
+	User NewUser `json:"user"`
+}
+
+// GetUser returns __createUserInput.User, and is useful for accessing the field via an interface.
+func (v *__createUserInput) GetUser() NewUser { return v.User }
+
 // __queryWithCustomMarshalInput is used internally by genqlient
 type __queryWithCustomMarshalInput struct {
 	Date time.Time `json:"-"`
@@ -1290,6 +1305,26 @@ type __queryWithVariablesInput struct {
 // GetId returns __queryWithVariablesInput.Id, and is useful for accessing the field via an interface.
 func (v *__queryWithVariablesInput) GetId() string { return v.Id }
 
+// createUserCreateUser includes the requested fields of the GraphQL type User.
+type createUserCreateUser struct {
+	Id   string `json:"id"`
+	Name string `json:"name"`
+}
+
+// GetId returns createUserCreateUser.Id, and is useful for accessing the field via an interface.
+func (v *createUserCreateUser) GetId() string { return v.Id }
+
+// GetName returns createUserCreateUser.Name, and is useful for accessing the field via an interface.
+func (v *createUserCreateUser) GetName() string { return v.Name }
+
+// createUserResponse is returned by createUser on success.
+type createUserResponse struct {
+	CreateUser createUserCreateUser `json:"createUser"`
+}
+
+// GetCreateUser returns createUserResponse.CreateUser, and is useful for accessing the field via an interface.
+func (v *createUserResponse) GetCreateUser() createUserCreateUser { return v.CreateUser }
+
 // failingQueryMeUser includes the requested fields of the GraphQL type User.
 type failingQueryMeUser struct {
 	Id string `json:"id"`
@@ -3043,6 +3078,39 @@ type simpleQueryResponse struct {
 // GetMe returns simpleQueryResponse.Me, and is useful for accessing the field via an interface.
 func (v *simpleQueryResponse) GetMe() simpleQueryMeUser { return v.Me }
 
+func createUser(
+	ctx context.Context,
+	client graphql.Client,
+	user NewUser,
+) (*createUserResponse, map[string]interface{}, error) {
+	req := &graphql.Request{
+		OpName: "createUser",
+		Query: `
+mutation createUser ($user: NewUser!) {
+	createUser(input: $user) {
+		id
+		name
+	}
+}
+`,
+		Variables: &__createUserInput{
+			User: user,
+		},
+	}
+	var err error
+
+	var data createUserResponse
+	resp := &graphql.Response{Data: &data}
+
+	err = client.MakeRequest(
+		ctx,
+		req,
+		resp,
+	)
+
+	return &data, resp.Extensions, err
+}
+
 func failingQuery(
 	ctx context.Context,
 	client graphql.Client,
diff --git a/internal/integration/roundtrip.go b/internal/integration/roundtrip.go
index 4653b18..a7b0c8d 100644
--- a/internal/integration/roundtrip.go
+++ b/internal/integration/roundtrip.go
@@ -101,10 +101,25 @@ func (c *roundtripClient) MakeRequest(ctx context.Context, req *graphql.Request,
 	return nil
 }
 
+func newRoundtripClients(t *testing.T, endpoint string) []graphql.Client {
+	return []graphql.Client{newRoundtripClient(t, endpoint), newRoundtripGetClient(t, endpoint)}
+}
+
 func newRoundtripClient(t *testing.T, endpoint string) graphql.Client {
 	transport := &lastResponseTransport{wrapped: http.DefaultTransport}
+	httpClient := &http.Client{Transport: transport}
+	return &roundtripClient{
+		wrapped:   graphql.NewClient(endpoint, httpClient),
+		transport: transport,
+		t:         t,
+	}
+}
+
+func newRoundtripGetClient(t *testing.T, endpoint string) graphql.Client {
+	transport := &lastResponseTransport{wrapped: http.DefaultTransport}
+	httpClient := &http.Client{Transport: transport}
 	return &roundtripClient{
-		wrapped:   graphql.NewClient(endpoint, &http.Client{Transport: transport}),
+		wrapped:   graphql.NewClientUsingGet(endpoint, httpClient),
 		transport: transport,
 		t:         t,
 	}
diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql
index 6d91862..7857a4a 100644
--- a/internal/integration/schema.graphql
+++ b/internal/integration/schema.graphql
@@ -12,6 +12,10 @@ type Query {
   fail: Boolean
 }
 
+type Mutation {
+  createUser(input: NewUser!): User!
+}
+
 type User implements Being & Lucky {
   id: ID!
   name: String!
@@ -21,6 +25,10 @@ type User implements Being & Lucky {
   friends: [User!]!
 }
 
+input NewUser {
+    name: String!
+}
+
 type Hair { color: String }   # silly name to confuse the name-generator
 
 type Animal implements Being {
diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go
index d0e4fd9..b73090f 100644
--- a/internal/integration/server/gqlgen_exec.go
+++ b/internal/integration/server/gqlgen_exec.go
@@ -35,6 +35,7 @@ type Config struct {
 }
 
 type ResolverRoot interface {
+	Mutation() MutationResolver
 	Query() QueryResolver
 }
 
@@ -58,6 +59,10 @@ type ComplexityRoot struct {
 		Color func(childComplexity int) int
 	}
 
+	Mutation struct {
+		CreateUser func(childComplexity int, input NewUser) int
+	}
+
 	Query struct {
 		Being            func(childComplexity int, id string) int
 		Beings           func(childComplexity int, ids []string) int
@@ -80,6 +85,9 @@ type ComplexityRoot struct {
 	}
 }
 
+type MutationResolver interface {
+	CreateUser(ctx context.Context, input NewUser) (*User, error)
+}
 type QueryResolver interface {
 	Me(ctx context.Context) (*User, error)
 	User(ctx context.Context, id *string) (*User, error)
@@ -156,6 +164,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Hair.Color(childComplexity), true
 
+	case "Mutation.createUser":
+		if e.complexity.Mutation.CreateUser == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_createUser_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.CreateUser(childComplexity, args["input"].(NewUser)), true
+
 	case "Query.being":
 		if e.complexity.Query.Being == nil {
 			break
@@ -316,6 +336,20 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
 			var buf bytes.Buffer
 			data.MarshalGQL(&buf)
 
+			return &graphql.Response{
+				Data: buf.Bytes(),
+			}
+		}
+	case ast.Mutation:
+		return func(ctx context.Context) *graphql.Response {
+			if !first {
+				return nil
+			}
+			first = false
+			data := ec._Mutation(ctx, rc.Operation.SelectionSet)
+			var buf bytes.Buffer
+			data.MarshalGQL(&buf)
+
 			return &graphql.Response{
 				Data: buf.Bytes(),
 			}
@@ -360,6 +394,10 @@ type Query {
   fail: Boolean
 }
 
+type Mutation {
+  createUser(input: NewUser!): User!
+}
+
 type User implements Being & Lucky {
   id: ID!
   name: String!
@@ -369,6 +407,10 @@ type User implements Being & Lucky {
   friends: [User!]!
 }
 
+input NewUser {
+    name: String!
+}
+
 type Hair { color: String }   # silly name to confuse the name-generator
 
 type Animal implements Being {
@@ -402,6 +444,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...)
 
 // region    ***************************** args.gotpl *****************************
 
+func (ec *executionContext) field_Mutation_createUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 NewUser
+	if tmp, ok := rawArgs["input"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+		arg0, err = ec.unmarshalNNewUser2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐNewUser(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["input"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -805,6 +862,48 @@ func (ec *executionContext) _Hair_color(ctx context.Context, field graphql.Colle
 	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "Mutation",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   true,
+		IsResolver: true,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Mutation_createUser_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	fc.Args = args
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Mutation().CreateUser(rctx, args["input"].(NewUser))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*User)
+	fc.Result = res
+	return ec.marshalNUser2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -2609,6 +2708,29 @@ func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field gr
 
 // region    **************************** input.gotpl *****************************
 
+func (ec *executionContext) unmarshalInputNewUser(ctx context.Context, obj interface{}) (NewUser, error) {
+	var it NewUser
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	for k, v := range asMap {
+		switch k {
+		case "name":
+			var err error
+
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
+			it.Name, err = ec.unmarshalNString2string(ctx, v)
+			if err != nil {
+				return it, err
+			}
+		}
+	}
+
+	return it, nil
+}
+
 // endregion **************************** input.gotpl *****************************
 
 // region    ************************** interface.gotpl ***************************
@@ -2780,6 +2902,46 @@ func (ec *executionContext) _Hair(ctx context.Context, sel ast.SelectionSet, obj
 	return out
 }
 
+var mutationImplementors = []string{"Mutation"}
+
+func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors)
+	ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
+		Object: "Mutation",
+	})
+
+	out := graphql.NewFieldSet(fields)
+	var invalids uint32
+	for i, field := range fields {
+		innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{
+			Object: field.Name,
+			Field:  field,
+		})
+
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Mutation")
+		case "createUser":
+			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Mutation_createUser(ctx, field)
+			}
+
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, innerFunc)
+
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch()
+	if invalids > 0 {
+		return graphql.Null
+	}
+	return out
+}
+
 var queryImplementors = []string{"Query"}
 
 func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@@ -3670,6 +3832,11 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti
 	return res
 }
 
+func (ec *executionContext) unmarshalNNewUser2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐNewUser(ctx context.Context, v interface{}) (NewUser, error) {
+	res, err := ec.unmarshalInputNewUser(ctx, v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
 func (ec *executionContext) unmarshalNSpecies2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐSpecies(ctx context.Context, v interface{}) (Species, error) {
 	var res Species
 	err := res.UnmarshalGQL(v)
@@ -3695,6 +3862,10 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
 	return res
 }
 
+func (ec *executionContext) marshalNUser2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx context.Context, sel ast.SelectionSet, v User) graphql.Marshaler {
+	return ec._User(ctx, sel, &v)
+}
+
 func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*User) graphql.Marshaler {
 	ret := make(graphql.Array, len(v))
 	var wg sync.WaitGroup
diff --git a/internal/integration/server/gqlgen_models.go b/internal/integration/server/gqlgen_models.go
index 73ae392..0a8ac66 100644
--- a/internal/integration/server/gqlgen_models.go
+++ b/internal/integration/server/gqlgen_models.go
@@ -34,6 +34,10 @@ type Hair struct {
 	Color *string `json:"color"`
 }
 
+type NewUser struct {
+	Name string `json:"name"`
+}
+
 type User struct {
 	ID          string  `json:"id"`
 	Name        string  `json:"name"`
diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go
index e94adb4..00dbb30 100644
--- a/internal/integration/server/server.go
+++ b/internal/integration/server/server.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"net/http/httptest"
+	"strconv"
 
 	"github.com/99designs/gqlgen/graphql"
 	"github.com/99designs/gqlgen/graphql/handler"
@@ -73,6 +74,24 @@ func beingByID(id string) Being {
 	return nil
 }
 
+func getNewID() string {
+	maxID := 0
+	for _, user := range users {
+		intID, _ := strconv.Atoi(user.ID)
+		if intID > maxID {
+			maxID = intID
+		}
+	}
+	for _, animal := range animals {
+		intID, _ := strconv.Atoi(animal.ID)
+		if intID > maxID {
+			maxID = intID
+		}
+	}
+	newID := maxID + 1
+	return strconv.Itoa(newID)
+}
+
 func (r *queryResolver) Me(ctx context.Context) (*User, error) {
 	return userByID("1"), nil
 }
@@ -129,9 +148,16 @@ func (r *queryResolver) Fail(ctx context.Context) (*bool, error) {
 	return &f, fmt.Errorf("oh no")
 }
 
+func (m mutationResolver) CreateUser(ctx context.Context, input NewUser) (*User, error) {
+	newUser := User{ID: getNewID(), Name: input.Name, Friends: []*User{}}
+	users = append(users, &newUser)
+	return &newUser, nil
+}
+
 func RunServer() *httptest.Server {
 	gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}}))
 	gqlgenServer.AddTransport(transport.POST{})
+	gqlgenServer.AddTransport(transport.GET{})
 	gqlgenServer.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
 		graphql.RegisterExtension(ctx, "foobar", "test")
 		return next(ctx)
@@ -140,10 +166,15 @@ func RunServer() *httptest.Server {
 }
 
 type (
-	resolver      struct{}
-	queryResolver struct{}
+	resolver         struct{}
+	queryResolver    struct{}
+	mutationResolver struct{}
 )
 
+func (r *resolver) Mutation() MutationResolver {
+	return &mutationResolver{}
+}
+
 func (r *resolver) Query() QueryResolver { return &queryResolver{} }
 
 //go:generate go run github.com/99designs/gqlgen


Test patch:
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index e41ced5..0e056fa 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -25,14 +25,35 @@ func TestSimpleQuery(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
+	clients := newRoundtripClients(t, server.URL)
 
-	resp, _, err := simpleQuery(ctx, client)
+	for _, client := range clients {
+		resp, _, err := simpleQuery(ctx, client)
+		require.NoError(t, err)
+
+		assert.Equal(t, "1", resp.Me.Id)
+		assert.Equal(t, "Yours Truly", resp.Me.Name)
+		assert.Equal(t, 17, resp.Me.LuckyNumber)
+	}
+}
+
+func TestMutation(t *testing.T) {
+	_ = `# @genqlient
+	mutation createUser($user: NewUser!) { createUser(input: $user) { id name } }`
+
+	ctx := context.Background()
+	server := server.RunServer()
+	defer server.Close()
+	postClient := newRoundtripClient(t, server.URL)
+	getClient := newRoundtripGetClient(t, server.URL)
+
+	resp, _, err := createUser(ctx, postClient, NewUser{Name: "Jack"})
 	require.NoError(t, err)
+	assert.Equal(t, "5", resp.CreateUser.Id)
+	assert.Equal(t, "Jack", resp.CreateUser.Name)
 
-	assert.Equal(t, "1", resp.Me.Id)
-	assert.Equal(t, "Yours Truly", resp.Me.Name)
-	assert.Equal(t, 17, resp.Me.LuckyNumber)
+	_, _, err = createUser(ctx, getClient, NewUser{Name: "Jill"})
+	require.Errorf(t, err, "client does not support mutations")
 }
 
 func TestServerError(t *testing.T) {
@@ -42,30 +63,34 @@ func TestServerError(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
-
-	resp, _, err := failingQuery(ctx, client)
-	// As long as we get some response back, we should still return a full
-	// response -- and indeed in this case it should even have another field
-	// (which didn't err) set.
-	assert.Error(t, err)
-	assert.NotNil(t, resp)
-	assert.Equal(t, "1", resp.Me.Id)
+	clients := newRoundtripClients(t, server.URL)
+
+	for _, client := range clients {
+		resp, _, err := failingQuery(ctx, client)
+		// As long as we get some response back, we should still return a full
+		// response -- and indeed in this case it should even have another field
+		// (which didn't err) set.
+		assert.Error(t, err)
+		assert.NotNil(t, resp)
+		assert.Equal(t, "1", resp.Me.Id)
+	}
 }
 
 func TestNetworkError(t *testing.T) {
 	ctx := context.Background()
-	client := newRoundtripClient(t, "https://nothing.invalid/graphql")
-
-	resp, _, err := failingQuery(ctx, client)
-	// As we guarantee in the README, even on network error you always get a
-	// non-nil response; this is so you can write e.g.
-	//	resp, err := failingQuery(ctx)
-	//	return resp.Me.Id, err
-	// without a bunch of extra ceremony.
-	assert.Error(t, err)
-	assert.NotNil(t, resp)
-	assert.Equal(t, new(failingQueryResponse), resp)
+	clients := newRoundtripClients(t, "https://nothing.invalid/graphql")
+
+	for _, client := range clients {
+		resp, _, err := failingQuery(ctx, client)
+		// As we guarantee in the README, even on network error you always get a
+		// non-nil response; this is so you can write e.g.
+		//	resp, err := failingQuery(ctx)
+		//	return resp.Me.Id, err
+		// without a bunch of extra ceremony.
+		assert.Error(t, err)
+		assert.NotNil(t, resp)
+		assert.Equal(t, new(failingQueryResponse), resp)
+	}
 }
 
 func TestVariables(t *testing.T) {
@@ -80,19 +105,24 @@ func TestVariables(t *testing.T) {
 	// this right in Go (without adding `pointer: true` just for this purpose),
 	// and unmarshal(marshal(resp)) == resp should still hold, so we don't
 	// worry about it.
-	client := graphql.NewClient(server.URL, http.DefaultClient)
+	clients := []graphql.Client{
+		graphql.NewClient(server.URL, http.DefaultClient),
+		graphql.NewClientUsingGet(server.URL, http.DefaultClient),
+	}
 
-	resp, _, err := queryWithVariables(ctx, client, "2")
-	require.NoError(t, err)
+	for _, client := range clients {
+		resp, _, err := queryWithVariables(ctx, client, "2")
+		require.NoError(t, err)
 
-	assert.Equal(t, "2", resp.User.Id)
-	assert.Equal(t, "Raven", resp.User.Name)
-	assert.Equal(t, -1, resp.User.LuckyNumber)
+		assert.Equal(t, "2", resp.User.Id)
+		assert.Equal(t, "Raven", resp.User.Name)
+		assert.Equal(t, -1, resp.User.LuckyNumber)
 
-	resp, _, err = queryWithVariables(ctx, client, "374892379482379")
-	require.NoError(t, err)
+		resp, _, err = queryWithVariables(ctx, client, "374892379482379")
+		require.NoError(t, err)
 
-	assert.Zero(t, resp.User)
+		assert.Zero(t, resp.User)
+	}
 }
 
 func TestExtensions(t *testing.T) {
@@ -102,12 +132,14 @@ func TestExtensions(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
+	clients := newRoundtripClients(t, server.URL)
 
-	_, extensions, err := simpleQueryExt(ctx, client)
-	require.NoError(t, err)
-	assert.NotNil(t, extensions)
-	assert.Equal(t, extensions["foobar"], "test")
+	for _, client := range clients {
+		_, extensions, err := simpleQueryExt(ctx, client)
+		require.NoError(t, err)
+		assert.NotNil(t, extensions)
+		assert.Equal(t, extensions["foobar"], "test")
+	}
 }
 
 func TestOmitempty(t *testing.T) {
@@ -119,22 +151,24 @@ func TestOmitempty(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
+	clients := newRoundtripClients(t, server.URL)
 
-	resp, _, err := queryWithOmitempty(ctx, client, "2")
-	require.NoError(t, err)
+	for _, client := range clients {
+		resp, _, err := queryWithOmitempty(ctx, client, "2")
+		require.NoError(t, err)
 
-	assert.Equal(t, "2", resp.User.Id)
-	assert.Equal(t, "Raven", resp.User.Name)
-	assert.Equal(t, -1, resp.User.LuckyNumber)
+		assert.Equal(t, "2", resp.User.Id)
+		assert.Equal(t, "Raven", resp.User.Name)
+		assert.Equal(t, -1, resp.User.LuckyNumber)
 
-	// should return default user, not the user with ID ""
-	resp, _, err = queryWithOmitempty(ctx, client, "")
-	require.NoError(t, err)
+		// should return default user, not the user with ID ""
+		resp, _, err = queryWithOmitempty(ctx, client, "")
+		require.NoError(t, err)
 
-	assert.Equal(t, "1", resp.User.Id)
-	assert.Equal(t, "Yours Truly", resp.User.Name)
-	assert.Equal(t, 17, resp.User.LuckyNumber)
+		assert.Equal(t, "1", resp.User.Id)
+		assert.Equal(t, "Yours Truly", resp.User.Name)
+		assert.Equal(t, 17, resp.User.LuckyNumber)
+	}
 }
 
 func TestCustomMarshal(t *testing.T) {
@@ -146,24 +180,26 @@ func TestCustomMarshal(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
-
-	resp, _, err := queryWithCustomMarshal(ctx, client,
-		time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC))
-	require.NoError(t, err)
-
-	assert.Len(t, resp.UsersBornOn, 1)
-	user := resp.UsersBornOn[0]
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "Yours Truly", user.Name)
-	assert.Equal(t,
-		time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
-		user.Birthdate)
-
-	resp, _, err = queryWithCustomMarshal(ctx, client,
-		time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC))
-	require.NoError(t, err)
-	assert.Len(t, resp.UsersBornOn, 0)
+	clients := newRoundtripClients(t, server.URL)
+
+	for _, client := range clients {
+		resp, _, err := queryWithCustomMarshal(ctx, client,
+			time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC))
+		require.NoError(t, err)
+
+		assert.Len(t, resp.UsersBornOn, 1)
+		user := resp.UsersBornOn[0]
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "Yours Truly", user.Name)
+		assert.Equal(t,
+			time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
+			user.Birthdate)
+
+		resp, _, err = queryWithCustomMarshal(ctx, client,
+			time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC))
+		require.NoError(t, err)
+		assert.Len(t, resp.UsersBornOn, 0)
+	}
 }
 
 func TestCustomMarshalSlice(t *testing.T) {
@@ -175,24 +211,26 @@ func TestCustomMarshalSlice(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
-
-	resp, _, err := queryWithCustomMarshalSlice(ctx, client,
-		[]time.Time{time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)})
-	require.NoError(t, err)
-
-	assert.Len(t, resp.UsersBornOnDates, 1)
-	user := resp.UsersBornOnDates[0]
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "Yours Truly", user.Name)
-	assert.Equal(t,
-		time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
-		user.Birthdate)
-
-	resp, _, err = queryWithCustomMarshalSlice(ctx, client,
-		[]time.Time{time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)})
-	require.NoError(t, err)
-	assert.Len(t, resp.UsersBornOnDates, 0)
+	clients := newRoundtripClients(t, server.URL)
+
+	for _, client := range clients {
+		resp, _, err := queryWithCustomMarshalSlice(ctx, client,
+			[]time.Time{time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)})
+		require.NoError(t, err)
+
+		assert.Len(t, resp.UsersBornOnDates, 1)
+		user := resp.UsersBornOnDates[0]
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "Yours Truly", user.Name)
+		assert.Equal(t,
+			time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
+			user.Birthdate)
+
+		resp, _, err = queryWithCustomMarshalSlice(ctx, client,
+			[]time.Time{time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)})
+		require.NoError(t, err)
+		assert.Len(t, resp.UsersBornOnDates, 0)
+	}
 }
 
 func TestCustomMarshalOptional(t *testing.T) {
@@ -209,28 +247,30 @@ func TestCustomMarshalOptional(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
-
-	date := time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)
-	resp, _, err := queryWithCustomMarshalOptional(ctx, client, &date, nil)
-	require.NoError(t, err)
-
-	assert.Len(t, resp.UserSearch, 1)
-	user := resp.UserSearch[0]
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "Yours Truly", user.Name)
-	assert.Equal(t,
-		time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
-		user.Birthdate)
-
-	id := "2"
-	resp, _, err = queryWithCustomMarshalOptional(ctx, client, nil, &id)
-	require.NoError(t, err)
-	assert.Len(t, resp.UserSearch, 1)
-	user = resp.UserSearch[0]
-	assert.Equal(t, "2", user.Id)
-	assert.Equal(t, "Raven", user.Name)
-	assert.Zero(t, user.Birthdate)
+	clients := newRoundtripClients(t, server.URL)
+
+	for _, client := range clients {
+		date := time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)
+		resp, _, err := queryWithCustomMarshalOptional(ctx, client, &date, nil)
+		require.NoError(t, err)
+
+		assert.Len(t, resp.UserSearch, 1)
+		user := resp.UserSearch[0]
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "Yours Truly", user.Name)
+		assert.Equal(t,
+			time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
+			user.Birthdate)
+
+		id := "2"
+		resp, _, err = queryWithCustomMarshalOptional(ctx, client, nil, &id)
+		require.NoError(t, err)
+		assert.Len(t, resp.UserSearch, 1)
+		user = resp.UserSearch[0]
+		assert.Equal(t, "2", user.Id)
+		assert.Equal(t, "Raven", user.Name)
+		assert.Zero(t, user.Birthdate)
+	}
 }
 
 func TestInterfaceNoFragments(t *testing.T) {
@@ -243,58 +283,60 @@ func TestInterfaceNoFragments(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
+	clients := newRoundtripClients(t, server.URL)
 
-	resp, _, err := queryWithInterfaceNoFragments(ctx, client, "1")
-	require.NoError(t, err)
+	for _, client := range clients {
+		resp, _, err := queryWithInterfaceNoFragments(ctx, client, "1")
+		require.NoError(t, err)
 
-	// We should get the following response:
-	//	me: User{Id: 1, Name: "Yours Truly"},
-	//	being: User{Id: 1, Name: "Yours Truly"},
+		// We should get the following response:
+		//	me: User{Id: 1, Name: "Yours Truly"},
+		//	being: User{Id: 1, Name: "Yours Truly"},
 
-	assert.Equal(t, "1", resp.Me.Id)
-	assert.Equal(t, "Yours Truly", resp.Me.Name)
+		assert.Equal(t, "1", resp.Me.Id)
+		assert.Equal(t, "Yours Truly", resp.Me.Name)
 
-	// Check fields both via interface and via type-assertion:
-	assert.Equal(t, "User", resp.Being.GetTypename())
-	assert.Equal(t, "1", resp.Being.GetId())
-	assert.Equal(t, "Yours Truly", resp.Being.GetName())
+		// Check fields both via interface and via type-assertion:
+		assert.Equal(t, "User", resp.Being.GetTypename())
+		assert.Equal(t, "1", resp.Being.GetId())
+		assert.Equal(t, "Yours Truly", resp.Being.GetName())
 
-	user, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingUser)
-	require.Truef(t, ok, "got %T, not User", resp.Being)
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "Yours Truly", user.Name)
+		user, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingUser)
+		require.Truef(t, ok, "got %T, not User", resp.Being)
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "Yours Truly", user.Name)
 
-	resp, _, err = queryWithInterfaceNoFragments(ctx, client, "3")
-	require.NoError(t, err)
+		resp, _, err = queryWithInterfaceNoFragments(ctx, client, "3")
+		require.NoError(t, err)
 
-	// We should get the following response:
-	//	me: User{Id: 1, Name: "Yours Truly"},
-	//	being: Animal{Id: 3, Name: "Fido"},
+		// We should get the following response:
+		//	me: User{Id: 1, Name: "Yours Truly"},
+		//	being: Animal{Id: 3, Name: "Fido"},
 
-	assert.Equal(t, "1", resp.Me.Id)
-	assert.Equal(t, "Yours Truly", resp.Me.Name)
+		assert.Equal(t, "1", resp.Me.Id)
+		assert.Equal(t, "Yours Truly", resp.Me.Name)
 
-	assert.Equal(t, "Animal", resp.Being.GetTypename())
-	assert.Equal(t, "3", resp.Being.GetId())
-	assert.Equal(t, "Fido", resp.Being.GetName())
+		assert.Equal(t, "Animal", resp.Being.GetTypename())
+		assert.Equal(t, "3", resp.Being.GetId())
+		assert.Equal(t, "Fido", resp.Being.GetName())
 
-	animal, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingAnimal)
-	require.Truef(t, ok, "got %T, not Animal", resp.Being)
-	assert.Equal(t, "3", animal.Id)
-	assert.Equal(t, "Fido", animal.Name)
+		animal, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingAnimal)
+		require.Truef(t, ok, "got %T, not Animal", resp.Being)
+		assert.Equal(t, "3", animal.Id)
+		assert.Equal(t, "Fido", animal.Name)
 
-	resp, _, err = queryWithInterfaceNoFragments(ctx, client, "4757233945723")
-	require.NoError(t, err)
+		resp, _, err = queryWithInterfaceNoFragments(ctx, client, "4757233945723")
+		require.NoError(t, err)
 
-	// We should get the following response:
-	//	me: User{Id: 1, Name: "Yours Truly"},
-	//	being: null
+		// We should get the following response:
+		//	me: User{Id: 1, Name: "Yours Truly"},
+		//	being: null
 
-	assert.Equal(t, "1", resp.Me.Id)
-	assert.Equal(t, "Yours Truly", resp.Me.Name)
+		assert.Equal(t, "1", resp.Me.Id)
+		assert.Equal(t, "Yours Truly", resp.Me.Name)
 
-	assert.Nil(t, resp.Being)
+		assert.Nil(t, resp.Being)
+	}
 }
 
 func TestInterfaceListField(t *testing.T) {
@@ -306,39 +348,41 @@ func TestInterfaceListField(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
+	clients := newRoundtripClients(t, server.URL)
 
-	resp, _, err := queryWithInterfaceListField(ctx, client,
-		[]string{"1", "3", "12847394823"})
-	require.NoError(t, err)
+	for _, client := range clients {
+		resp, _, err := queryWithInterfaceListField(ctx, client,
+			[]string{"1", "3", "12847394823"})
+		require.NoError(t, err)
 
-	require.Len(t, resp.Beings, 3)
+		require.Len(t, resp.Beings, 3)
 
-	// We should get the following three beings:
-	//	User{Id: 1, Name: "Yours Truly"},
-	//	Animal{Id: 3, Name: "Fido"},
-	//	null
+		// We should get the following three beings:
+		//	User{Id: 1, Name: "Yours Truly"},
+		//	Animal{Id: 3, Name: "Fido"},
+		//	null
 
-	// Check fields both via interface and via type-assertion:
-	assert.Equal(t, "User", resp.Beings[0].GetTypename())
-	assert.Equal(t, "1", resp.Beings[0].GetId())
-	assert.Equal(t, "Yours Truly", resp.Beings[0].GetName())
+		// Check fields both via interface and via type-assertion:
+		assert.Equal(t, "User", resp.Beings[0].GetTypename())
+		assert.Equal(t, "1", resp.Beings[0].GetId())
+		assert.Equal(t, "Yours Truly", resp.Beings[0].GetName())
 
-	user, ok := resp.Beings[0].(*queryWithInterfaceListFieldBeingsUser)
-	require.Truef(t, ok, "got %T, not User", resp.Beings[0])
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "Yours Truly", user.Name)
+		user, ok := resp.Beings[0].(*queryWithInterfaceListFieldBeingsUser)
+		require.Truef(t, ok, "got %T, not User", resp.Beings[0])
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "Yours Truly", user.Name)
 
-	assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
-	assert.Equal(t, "3", resp.Beings[1].GetId())
-	assert.Equal(t, "Fido", resp.Beings[1].GetName())
+		assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
+		assert.Equal(t, "3", resp.Beings[1].GetId())
+		assert.Equal(t, "Fido", resp.Beings[1].GetName())
 
-	animal, ok := resp.Beings[1].(*queryWithInterfaceListFieldBeingsAnimal)
-	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
-	assert.Equal(t, "3", animal.Id)
-	assert.Equal(t, "Fido", animal.Name)
+		animal, ok := resp.Beings[1].(*queryWithInterfaceListFieldBeingsAnimal)
+		require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+		assert.Equal(t, "3", animal.Id)
+		assert.Equal(t, "Fido", animal.Name)
 
-	assert.Nil(t, resp.Beings[2])
+		assert.Nil(t, resp.Beings[2])
+	}
 }
 
 func TestInterfaceListPointerField(t *testing.T) {
@@ -353,34 +397,36 @@ func TestInterfaceListPointerField(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
+	clients := newRoundtripClients(t, server.URL)
 
-	resp, _, err := queryWithInterfaceListPointerField(ctx, client,
-		[]string{"1", "3", "12847394823"})
-	require.NoError(t, err)
+	for _, client := range clients {
+		resp, _, err := queryWithInterfaceListPointerField(ctx, client,
+			[]string{"1", "3", "12847394823"})
+		require.NoError(t, err)
 
-	require.Len(t, resp.Beings, 3)
+		require.Len(t, resp.Beings, 3)
 
-	// Check fields both via interface and via type-assertion:
-	assert.Equal(t, "User", (*resp.Beings[0]).GetTypename())
-	assert.Equal(t, "1", (*resp.Beings[0]).GetId())
-	assert.Equal(t, "Yours Truly", (*resp.Beings[0]).GetName())
+		// Check fields both via interface and via type-assertion:
+		assert.Equal(t, "User", (*resp.Beings[0]).GetTypename())
+		assert.Equal(t, "1", (*resp.Beings[0]).GetId())
+		assert.Equal(t, "Yours Truly", (*resp.Beings[0]).GetName())
 
-	user, ok := (*resp.Beings[0]).(*queryWithInterfaceListPointerFieldBeingsUser)
-	require.Truef(t, ok, "got %T, not User", *resp.Beings[0])
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "Yours Truly", user.Name)
+		user, ok := (*resp.Beings[0]).(*queryWithInterfaceListPointerFieldBeingsUser)
+		require.Truef(t, ok, "got %T, not User", *resp.Beings[0])
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "Yours Truly", user.Name)
 
-	assert.Equal(t, "Animal", (*resp.Beings[1]).GetTypename())
-	assert.Equal(t, "3", (*resp.Beings[1]).GetId())
-	assert.Equal(t, "Fido", (*resp.Beings[1]).GetName())
+		assert.Equal(t, "Animal", (*resp.Beings[1]).GetTypename())
+		assert.Equal(t, "3", (*resp.Beings[1]).GetId())
+		assert.Equal(t, "Fido", (*resp.Beings[1]).GetName())
 
-	animal, ok := (*resp.Beings[1]).(*queryWithInterfaceListPointerFieldBeingsAnimal)
-	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
-	assert.Equal(t, "3", animal.Id)
-	assert.Equal(t, "Fido", animal.Name)
+		animal, ok := (*resp.Beings[1]).(*queryWithInterfaceListPointerFieldBeingsAnimal)
+		require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+		assert.Equal(t, "3", animal.Id)
+		assert.Equal(t, "Fido", animal.Name)
 
-	assert.Nil(t, resp.Beings[2])
+		assert.Nil(t, resp.Beings[2])
+	}
 }
 
 func TestFragments(t *testing.T) {
@@ -407,62 +453,64 @@ func TestFragments(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
-
-	resp, _, err := queryWithFragments(ctx, client, []string{"1", "3", "12847394823"})
-	require.NoError(t, err)
-
-	require.Len(t, resp.Beings, 3)
-
-	// We should get the following three beings:
-	//	User{Id: 1, Name: "Yours Truly"},
-	//	Animal{Id: 3, Name: "Fido"},
-	//	null
-
-	// Check fields both via interface and via type-assertion when possible
-	// User has, in total, the fields: __typename id name luckyNumber.
-	assert.Equal(t, "User", resp.Beings[0].GetTypename())
-	assert.Equal(t, "1", resp.Beings[0].GetId())
-	assert.Equal(t, "Yours Truly", resp.Beings[0].GetName())
-	// (hair and luckyNumber we need to cast for)
-
-	user, ok := resp.Beings[0].(*queryWithFragmentsBeingsUser)
-	require.Truef(t, ok, "got %T, not User", resp.Beings[0])
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "Yours Truly", user.Name)
-	assert.Equal(t, "Black", user.Hair.Color)
-	assert.Equal(t, 17, user.LuckyNumber)
-
-	// Animal has, in total, the fields:
-	//	__typename
-	//	id
-	//	species
-	//	owner {
-	//		id
-	//		name
-	//		... on User { luckyNumber }
-	//	}
-	assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
-	assert.Equal(t, "3", resp.Beings[1].GetId())
-	// (hair, species, and owner.* we have to cast for)
-
-	animal, ok := resp.Beings[1].(*queryWithFragmentsBeingsAnimal)
-	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
-	assert.Equal(t, "3", animal.Id)
-	assert.Equal(t, SpeciesDog, animal.Species)
-	assert.True(t, animal.Hair.HasHair)
-
-	assert.Equal(t, "1", animal.Owner.GetId())
-	assert.Equal(t, "Yours Truly", animal.Owner.GetName())
-	// (luckyNumber we have to cast for, again)
-
-	owner, ok := animal.Owner.(*queryWithFragmentsBeingsAnimalOwnerUser)
-	require.Truef(t, ok, "got %T, not User", animal.Owner)
-	assert.Equal(t, "1", owner.Id)
-	assert.Equal(t, "Yours Truly", owner.Name)
-	assert.Equal(t, 17, owner.LuckyNumber)
-
-	assert.Nil(t, resp.Beings[2])
+	clients := newRoundtripClients(t, server.URL)
+
+	for _, client := range clients {
+		resp, _, err := queryWithFragments(ctx, client, []string{"1", "3", "12847394823"})
+		require.NoError(t, err)
+
+		require.Len(t, resp.Beings, 3)
+
+		// We should get the following three beings:
+		//	User{Id: 1, Name: "Yours Truly"},
+		//	Animal{Id: 3, Name: "Fido"},
+		//	null
+
+		// Check fields both via interface and via type-assertion when possible
+		// User has, in total, the fields: __typename id name luckyNumber.
+		assert.Equal(t, "User", resp.Beings[0].GetTypename())
+		assert.Equal(t, "1", resp.Beings[0].GetId())
+		assert.Equal(t, "Yours Truly", resp.Beings[0].GetName())
+		// (hair and luckyNumber we need to cast for)
+
+		user, ok := resp.Beings[0].(*queryWithFragmentsBeingsUser)
+		require.Truef(t, ok, "got %T, not User", resp.Beings[0])
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "Yours Truly", user.Name)
+		assert.Equal(t, "Black", user.Hair.Color)
+		assert.Equal(t, 17, user.LuckyNumber)
+
+		// Animal has, in total, the fields:
+		//	__typename
+		//	id
+		//	species
+		//	owner {
+		//		id
+		//		name
+		//		... on User { luckyNumber }
+		//	}
+		assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
+		assert.Equal(t, "3", resp.Beings[1].GetId())
+		// (hair, species, and owner.* we have to cast for)
+
+		animal, ok := resp.Beings[1].(*queryWithFragmentsBeingsAnimal)
+		require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+		assert.Equal(t, "3", animal.Id)
+		assert.Equal(t, SpeciesDog, animal.Species)
+		assert.True(t, animal.Hair.HasHair)
+
+		assert.Equal(t, "1", animal.Owner.GetId())
+		assert.Equal(t, "Yours Truly", animal.Owner.GetName())
+		// (luckyNumber we have to cast for, again)
+
+		owner, ok := animal.Owner.(*queryWithFragmentsBeingsAnimalOwnerUser)
+		require.Truef(t, ok, "got %T, not User", animal.Owner)
+		assert.Equal(t, "1", owner.Id)
+		assert.Equal(t, "Yours Truly", owner.Name)
+		assert.Equal(t, 17, owner.LuckyNumber)
+
+		assert.Nil(t, resp.Beings[2])
+	}
 }
 
 func TestNamedFragments(t *testing.T) {
@@ -500,73 +548,75 @@ func TestNamedFragments(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
-
-	resp, _, err := queryWithNamedFragments(ctx, client, []string{"1", "3", "12847394823"})
-	require.NoError(t, err)
-
-	require.Len(t, resp.Beings, 3)
-
-	// We should get the following three beings:
-	//	User{Id: 1, Name: "Yours Truly"},
-	//	Animal{Id: 3, Name: "Fido"},
-	//	null
-
-	// Check fields both via interface and via type-assertion when possible
-	// User has, in total, the fields: __typename id luckyNumber.
-	assert.Equal(t, "User", resp.Beings[0].GetTypename())
-	assert.Equal(t, "1", resp.Beings[0].GetId())
-	// (luckyNumber, hair we need to cast for)
-
-	user, ok := resp.Beings[0].(*queryWithNamedFragmentsBeingsUser)
-	require.Truef(t, ok, "got %T, not User", resp.Beings[0])
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, "1", user.UserFields.Id)
-	assert.Equal(t, "1", user.UserFields.MoreUserFields.Id)
-	assert.Equal(t, "1", user.UserFields.LuckyFieldsUser.MoreUserFields.Id)
-	// on UserFields, but we should be able to access directly via embedding:
-	assert.Equal(t, 17, user.LuckyNumber)
-	assert.Equal(t, "Black", user.Hair.Color)
-	assert.Equal(t, "Black", user.UserFields.MoreUserFields.Hair.Color)
-	assert.Equal(t, "Black", user.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color)
-
-	// Animal has, in total, the fields:
-	//	__typename
-	//	id
-	//	hair { hasHair }
-	//	owner { id luckyNumber }
-	assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
-	assert.Equal(t, "3", resp.Beings[1].GetId())
-	// (hair.* and owner.* we have to cast for)
-
-	animal, ok := resp.Beings[1].(*queryWithNamedFragmentsBeingsAnimal)
-	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
-	// Check that we filled in *both* ID fields:
-	assert.Equal(t, "3", animal.Id)
-	assert.Equal(t, "3", animal.AnimalFields.Id)
-	// on AnimalFields:
-	assert.True(t, animal.Hair.HasHair)
-	assert.Equal(t, "1", animal.Owner.GetId())
-	// (luckyNumber we have to cast for, again)
-
-	owner, ok := animal.Owner.(*AnimalFieldsOwnerUser)
-	require.Truef(t, ok, "got %T, not User", animal.Owner)
-	// Check that we filled in *both* ID fields:
-	assert.Equal(t, "1", owner.Id)
-	assert.Equal(t, "1", owner.UserFields.Id)
-	assert.Equal(t, "1", owner.UserFields.MoreUserFields.Id)
-	assert.Equal(t, "1", owner.UserFields.LuckyFieldsUser.MoreUserFields.Id)
-	// on UserFields:
-	assert.Equal(t, 17, owner.LuckyNumber)
-	assert.Equal(t, "Black", owner.UserFields.MoreUserFields.Hair.Color)
-	assert.Equal(t, "Black", owner.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color)
-
-	// Lucky-based fields we can also get by casting to the fragment-interface.
-	luckyOwner, ok := animal.Owner.(LuckyFields)
-	require.Truef(t, ok, "got %T, not Lucky", animal.Owner)
-	assert.Equal(t, 17, luckyOwner.GetLuckyNumber())
-
-	assert.Nil(t, resp.Beings[2])
+	clients := newRoundtripClients(t, server.URL)
+
+	for _, client := range clients {
+		resp, _, err := queryWithNamedFragments(ctx, client, []string{"1", "3", "12847394823"})
+		require.NoError(t, err)
+
+		require.Len(t, resp.Beings, 3)
+
+		// We should get the following three beings:
+		//	User{Id: 1, Name: "Yours Truly"},
+		//	Animal{Id: 3, Name: "Fido"},
+		//	null
+
+		// Check fields both via interface and via type-assertion when possible
+		// User has, in total, the fields: __typename id luckyNumber.
+		assert.Equal(t, "User", resp.Beings[0].GetTypename())
+		assert.Equal(t, "1", resp.Beings[0].GetId())
+		// (luckyNumber, hair we need to cast for)
+
+		user, ok := resp.Beings[0].(*queryWithNamedFragmentsBeingsUser)
+		require.Truef(t, ok, "got %T, not User", resp.Beings[0])
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, "1", user.UserFields.Id)
+		assert.Equal(t, "1", user.UserFields.MoreUserFields.Id)
+		assert.Equal(t, "1", user.UserFields.LuckyFieldsUser.MoreUserFields.Id)
+		// on UserFields, but we should be able to access directly via embedding:
+		assert.Equal(t, 17, user.LuckyNumber)
+		assert.Equal(t, "Black", user.Hair.Color)
+		assert.Equal(t, "Black", user.UserFields.MoreUserFields.Hair.Color)
+		assert.Equal(t, "Black", user.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color)
+
+		// Animal has, in total, the fields:
+		//	__typename
+		//	id
+		//	hair { hasHair }
+		//	owner { id luckyNumber }
+		assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
+		assert.Equal(t, "3", resp.Beings[1].GetId())
+		// (hair.* and owner.* we have to cast for)
+
+		animal, ok := resp.Beings[1].(*queryWithNamedFragmentsBeingsAnimal)
+		require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+		// Check that we filled in *both* ID fields:
+		assert.Equal(t, "3", animal.Id)
+		assert.Equal(t, "3", animal.AnimalFields.Id)
+		// on AnimalFields:
+		assert.True(t, animal.Hair.HasHair)
+		assert.Equal(t, "1", animal.Owner.GetId())
+		// (luckyNumber we have to cast for, again)
+
+		owner, ok := animal.Owner.(*AnimalFieldsOwnerUser)
+		require.Truef(t, ok, "got %T, not User", animal.Owner)
+		// Check that we filled in *both* ID fields:
+		assert.Equal(t, "1", owner.Id)
+		assert.Equal(t, "1", owner.UserFields.Id)
+		assert.Equal(t, "1", owner.UserFields.MoreUserFields.Id)
+		assert.Equal(t, "1", owner.UserFields.LuckyFieldsUser.MoreUserFields.Id)
+		// on UserFields:
+		assert.Equal(t, 17, owner.LuckyNumber)
+		assert.Equal(t, "Black", owner.UserFields.MoreUserFields.Hair.Color)
+		assert.Equal(t, "Black", owner.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color)
+
+		// Lucky-based fields we can also get by casting to the fragment-interface.
+		luckyOwner, ok := animal.Owner.(LuckyFields)
+		require.Truef(t, ok, "got %T, not Lucky", animal.Owner)
+		assert.Equal(t, 17, luckyOwner.GetLuckyNumber())
+
+		assert.Nil(t, resp.Beings[2])
+	}
 }
 
 func TestFlatten(t *testing.T) {
@@ -629,54 +679,56 @@ func TestFlatten(t *testing.T) {
 	ctx := context.Background()
 	server := server.RunServer()
 	defer server.Close()
-	client := newRoundtripClient(t, server.URL)
-
-	resp, _, err := queryWithFlatten(ctx, client, []string{"1", "3", "12847394823"})
-	require.NoError(t, err)
-
-	require.Len(t, resp.Beings, 3)
-
-	// We should get the following three beings:
-	//	User{Id: 1, Name: "Yours Truly"},
-	//	Animal{Id: 3, Name: "Fido"},
-	//	null
-
-	// Check fields both via interface and via type-assertion when possible
-	// User has, in total, the fields: __typename id luckyNumber.
-	assert.Equal(t, "User", resp.Beings[0].GetTypename())
-	assert.Equal(t, "1", resp.Beings[0].GetId())
-	// (luckyNumber we need to cast for)
-
-	user, ok := resp.Beings[0].(*QueryFragmentBeingsUser)
-	require.Truef(t, ok, "got %T, not User", resp.Beings[0])
-	assert.Equal(t, "1", user.Id)
-	assert.Equal(t, 17, user.InnerLuckyFieldsUser.LuckyNumber)
-
-	// Animal has, in total, the fields:
-	//	__typename
-	//	id
-	//	owner { id name ... on User { friends { id name } } }
-	assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
-	assert.Equal(t, "3", resp.Beings[1].GetId())
-	// (owner.* we have to cast for)
-
-	animal, ok := resp.Beings[1].(*QueryFragmentBeingsAnimal)
-	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
-	assert.Equal(t, "3", animal.Id)
-	// on AnimalFields:
-	assert.Equal(t, "1", animal.Owner.GetId())
-	assert.Equal(t, "Yours Truly", animal.Owner.GetName())
-	// (friends.* we have to cast for, again)
-
-	owner, ok := animal.Owner.(*InnerBeingFieldsUser)
-	require.Truef(t, ok, "got %T, not User", animal.Owner)
-	assert.Equal(t, "1", owner.Id)
-	assert.Equal(t, "Yours Truly", owner.Name)
-	assert.Len(t, owner.Friends, 1)
-	assert.Equal(t, "2", owner.Friends[0].Id)
-	assert.Equal(t, "Raven", owner.Friends[0].Name)
-
-	assert.Nil(t, resp.Beings[2])
+	clients := newRoundtripClients(t, server.URL)
+
+	for _, client := range clients {
+		resp, _, err := queryWithFlatten(ctx, client, []string{"1", "3", "12847394823"})
+		require.NoError(t, err)
+
+		require.Len(t, resp.Beings, 3)
+
+		// We should get the following three beings:
+		//	User{Id: 1, Name: "Yours Truly"},
+		//	Animal{Id: 3, Name: "Fido"},
+		//	null
+
+		// Check fields both via interface and via type-assertion when possible
+		// User has, in total, the fields: __typename id luckyNumber.
+		assert.Equal(t, "User", resp.Beings[0].GetTypename())
+		assert.Equal(t, "1", resp.Beings[0].GetId())
+		// (luckyNumber we need to cast for)
+
+		user, ok := resp.Beings[0].(*QueryFragmentBeingsUser)
+		require.Truef(t, ok, "got %T, not User", resp.Beings[0])
+		assert.Equal(t, "1", user.Id)
+		assert.Equal(t, 17, user.InnerLuckyFieldsUser.LuckyNumber)
+
+		// Animal has, in total, the fields:
+		//	__typename
+		//	id
+		//	owner { id name ... on User { friends { id name } } }
+		assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
+		assert.Equal(t, "3", resp.Beings[1].GetId())
+		// (owner.* we have to cast for)
+
+		animal, ok := resp.Beings[1].(*QueryFragmentBeingsAnimal)
+		require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+		assert.Equal(t, "3", animal.Id)
+		// on AnimalFields:
+		assert.Equal(t, "1", animal.Owner.GetId())
+		assert.Equal(t, "Yours Truly", animal.Owner.GetName())
+		// (friends.* we have to cast for, again)
+
+		owner, ok := animal.Owner.(*InnerBeingFieldsUser)
+		require.Truef(t, ok, "got %T, not User", animal.Owner)
+		assert.Equal(t, "1", owner.Id)
+		assert.Equal(t, "Yours Truly", owner.Name)
+		assert.Len(t, owner.Friends, 1)
+		assert.Equal(t, "2", owner.Friends[0].Id)
+		assert.Equal(t, "Raven", owner.Friends[0].Name)
+
+		assert.Nil(t, resp.Beings[2])
+	}
 }
 
 func TestGeneratedCode(t *testing.T) {