Repository: 99designs/gqlgen
Pull request #3555 (2025-02-23 11:19:13)

Add support application/graphql-response+json
https://graphql.github.io/graphql-over-http/draft/
GraphQL over HTTP is `Draft` status.
but many clients appear to support this.

for example, apollo client switch `Accept` header default to `application/graphql-response+json`.
https://github.com/apollographql/apollo-client/issues/12206
Apollo Client 4.0 will be shipped at Mid April 2025
https://github.com/apollographql/apollo-client/blob/main/ROADMAP.md

also urql supports this.
https://github.com/urql-graphql/urql/releases/tag/%40urql%2Fcore%402.5.0

I have:
 - [x] Added tests covering the bug / feature (see [testing](https://github.com/99designs/gqlgen/blob/master/TESTING.md))
 - [x] Updated any relevant documentation (see [docs](https://github.com/99designs/gqlgen/tree/master/docs/content))


coveralls: 
[![Coverage Status](https://coveralls.io/builds/72381215/badge)](https://coveralls.io/builds/72381215)

coverage: 73.507% (+0.1%) from 73.4%
when pulling **0dfa5f3e04b5f9162ae441fbe1901f5ab5fe5b95 on vvakame:feat-graphql-response**
into **9baafe1742daaf9c97a3bdc65b2a543b5da286c1 on 99designs:master**.

StevenACoffman: Thanks so much for this! Always happy to see your work.

Patch:
diff --git a/graphql/handler/transport/headers.go b/graphql/handler/transport/headers.go
index bc4e5724..98924819 100644
--- a/graphql/handler/transport/headers.go
+++ b/graphql/handler/transport/headers.go
@@ -1,10 +1,51 @@
 package transport
 
-import "net/http"
+import (
+	"mime"
+	"net/http"
+	"strings"
+)
+
+const (
+	acceptApplicationJson                = "application/json"
+	acceptApplicationGraphqlResponseJson = "application/graphql-response+json"
+)
+
+func determineResponseContentType(explicitHeaders map[string][]string, r *http.Request) string {
+	for k, v := range explicitHeaders {
+		if strings.EqualFold(k, "Content-Type") {
+			return v[0]
+		}
+	}
+
+	accept := r.Header.Get("Accept")
+	if accept == "" {
+		return acceptApplicationGraphqlResponseJson
+	}
+
+	for _, acceptPart := range strings.Split(accept, ",") {
+		mediaType, _, err := mime.ParseMediaType(strings.TrimSpace(acceptPart))
+		if err != nil {
+			continue
+		}
+		switch mediaType {
+		case "*/*", "application/*":
+			return acceptApplicationGraphqlResponseJson
+		case "application/json":
+			return acceptApplicationJson
+		case "application/graphql-response+json":
+			return acceptApplicationGraphqlResponseJson
+		}
+	}
+
+	return acceptApplicationGraphqlResponseJson
+}
 
 func writeHeaders(w http.ResponseWriter, headers map[string][]string) {
 	if len(headers) == 0 {
 		headers = map[string][]string{
+			// Stay with application/json (not application/graphql-response+json)
+			// as it is not an actively supported protocol for now
 			"Content-Type": {"application/json"},
 		}
 	}
@@ -15,3 +56,14 @@ func writeHeaders(w http.ResponseWriter, headers map[string][]string) {
 		}
 	}
 }
+
+func mergeHeaders(baseHeaders, additionalHeaders map[string][]string) map[string][]string {
+	result := make(map[string][]string)
+	for k, v := range baseHeaders {
+		result[k] = v
+	}
+	for key, values := range additionalHeaders {
+		result[key] = values
+	}
+	return result
+}
diff --git a/graphql/handler/transport/http_get.go b/graphql/handler/transport/http_get.go
index 09f1020d..a3cffcc7 100644
--- a/graphql/handler/transport/http_get.go
+++ b/graphql/handler/transport/http_get.go
@@ -18,7 +18,7 @@ import (
 // defined in https://github.com/APIs-guru/graphql-over-http#get
 type GET struct {
 	// Map of all headers that are added to graphql response. If not
-	// set, only one header: Content-Type: application/json will be set.
+	// set, only one header: Content-Type: application/graphql-response+json will be set.
 	ResponseHeaders map[string][]string
 }
 
@@ -39,7 +39,14 @@ func (h GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecut
 		writeJsonError(w, err.Error())
 		return
 	}
-	writeHeaders(w, h.ResponseHeaders)
+	contentType := determineResponseContentType(h.ResponseHeaders, r)
+	responseHeaders := mergeHeaders(
+		map[string][]string{
+			"Content-Type": {contentType},
+		},
+		h.ResponseHeaders,
+	)
+	writeHeaders(w, responseHeaders)
 
 	raw := &graphql.RawParams{
 		Query:         query.Get("query"),
@@ -68,7 +75,11 @@ func (h GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecut
 
 	opCtx, gqlError := exec.CreateOperationContext(r.Context(), raw)
 	if gqlError != nil {
-		w.WriteHeader(statusFor(gqlError))
+		if contentType == acceptApplicationGraphqlResponseJson {
+			w.WriteHeader(statusForGraphQLResponse(gqlError))
+		} else {
+			w.WriteHeader(statusFor(gqlError))
+		}
 		resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), opCtx), gqlError)
 		writeJson(w, resp)
 		return
@@ -98,3 +109,13 @@ func statusFor(errs gqlerror.List) int {
 		return http.StatusOK
 	}
 }
+
+func statusForGraphQLResponse(errs gqlerror.List) int {
+	// https://graphql.github.io/graphql-over-http/draft/#sec-application-graphql-response-json
+	switch errcode.GetErrorKind(errs) {
+	case errcode.KindProtocol:
+		return http.StatusBadRequest
+	default:
+		return http.StatusOK
+	}
+}
diff --git a/graphql/handler/transport/http_post.go b/graphql/handler/transport/http_post.go
index 4b9a2399..34076b89 100644
--- a/graphql/handler/transport/http_post.go
+++ b/graphql/handler/transport/http_post.go
@@ -17,7 +17,7 @@ import (
 // defined in https://github.com/APIs-guru/graphql-over-http#post
 type POST struct {
 	// Map of all headers that are added to graphql response. If not
-	// set, only one header: Content-Type: application/json will be set.
+	// set, only one header: Content-Type: application/graphql-response+json will be set.
 	ResponseHeaders map[string][]string
 }
 
@@ -55,7 +55,14 @@ var pool = sync.Pool{
 
 func (h POST) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
 	ctx := r.Context()
-	writeHeaders(w, h.ResponseHeaders)
+	contentType := determineResponseContentType(h.ResponseHeaders, r)
+	responseHeaders := mergeHeaders(
+		map[string][]string{
+			"Content-Type": {contentType},
+		},
+		h.ResponseHeaders,
+	)
+	writeHeaders(w, responseHeaders)
 	params := pool.Get().(*graphql.RawParams)
 	defer func() {
 		params.Headers = nil
@@ -98,7 +105,11 @@ func (h POST) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecu
 
 	rc, opErr := exec.CreateOperationContext(ctx, params)
 	if opErr != nil {
-		w.WriteHeader(statusFor(opErr))
+		if contentType == acceptApplicationGraphqlResponseJson {
+			w.WriteHeader(statusForGraphQLResponse(opErr))
+		} else {
+			w.WriteHeader(statusFor(opErr))
+		}
 		resp := exec.DispatchError(graphql.WithOperationContext(ctx, rc), opErr)
 		writeJson(w, resp)
 		return


Test patch:
diff --git a/_examples/starwars/starwars_test.go b/_examples/starwars/starwars_test.go
index 01c707fc..d5a146bd 100644
--- a/_examples/starwars/starwars_test.go
+++ b/_examples/starwars/starwars_test.go
@@ -220,7 +220,7 @@ func TestStarwars(t *testing.T) {
 		  }
 		}`, &resp, client.Var("episode", "INVALID"))
 
-		require.EqualError(t, err, `http 422: {"errors":[{"message":"INVALID is not a valid Episode","path":["variable","episode"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
+		require.EqualError(t, err, `http 400: {"errors":[{"message":"INVALID is not a valid Episode","path":["variable","episode"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
 	})
 
 	t.Run("introspection", func(t *testing.T) {
diff --git a/codegen/testserver/followschema/enums_test.go b/codegen/testserver/followschema/enums_test.go
index da1d60ee..37588af3 100644
--- a/codegen/testserver/followschema/enums_test.go
+++ b/codegen/testserver/followschema/enums_test.go
@@ -40,7 +40,7 @@ func TestEnumsResolver(t *testing.T) {
 			enumInInput(input: {enum: INVALID})
 		}
 		`, &resp)
-		require.EqualError(t, err, `http 422: {"errors":[{"message":"Value \"INVALID\" does not exist in \"EnumTest!\" enum.","locations":[{"line":2,"column":30}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
+		require.EqualError(t, err, `http 400: {"errors":[{"message":"Value \"INVALID\" does not exist in \"EnumTest!\" enum.","locations":[{"line":2,"column":30}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
 	})
 
 	t.Run("input with invalid enum value via vars", func(t *testing.T) {
@@ -51,6 +51,6 @@ func TestEnumsResolver(t *testing.T) {
 			enumInInput(input: $input)
 		}
 		`, &resp, client.Var("input", map[string]any{"enum": "INVALID"}))
-		require.EqualError(t, err, `http 422: {"errors":[{"message":"INVALID is not a valid EnumTest","path":["variable","input","enum"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
+		require.EqualError(t, err, `http 400: {"errors":[{"message":"INVALID is not a valid EnumTest","path":["variable","input","enum"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
 	})
 }
diff --git a/codegen/testserver/followschema/input_test.go b/codegen/testserver/followschema/input_test.go
index 01b6d6ec..e8015595 100644
--- a/codegen/testserver/followschema/input_test.go
+++ b/codegen/testserver/followschema/input_test.go
@@ -30,7 +30,7 @@ func TestInput(t *testing.T) {
 
 		err := c.Post(`query { inputSlice(arg: ["ok", 1, 2, "ok"]) }`, &resp)
 
-		require.EqualError(t, err, `http 422: {"errors":[{"message":"String cannot represent a non string value: 1","locations":[{"line":1,"column":32}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"String cannot represent a non string value: 2","locations":[{"line":1,"column":35}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
+		require.EqualError(t, err, `http 400: {"errors":[{"message":"String cannot represent a non string value: 1","locations":[{"line":1,"column":32}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"String cannot represent a non string value: 2","locations":[{"line":1,"column":35}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
 		require.Nil(t, resp.DirectiveArg)
 	})
 
diff --git a/codegen/testserver/singlefile/enums_test.go b/codegen/testserver/singlefile/enums_test.go
index 031bfb31..ad63719e 100644
--- a/codegen/testserver/singlefile/enums_test.go
+++ b/codegen/testserver/singlefile/enums_test.go
@@ -40,7 +40,7 @@ func TestEnumsResolver(t *testing.T) {
 			enumInInput(input: {enum: INVALID})
 		}
 		`, &resp)
-		require.EqualError(t, err, `http 422: {"errors":[{"message":"Value \"INVALID\" does not exist in \"EnumTest!\" enum.","locations":[{"line":2,"column":30}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
+		require.EqualError(t, err, `http 400: {"errors":[{"message":"Value \"INVALID\" does not exist in \"EnumTest!\" enum.","locations":[{"line":2,"column":30}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
 	})
 
 	t.Run("input with invalid enum value via vars", func(t *testing.T) {
@@ -51,6 +51,6 @@ func TestEnumsResolver(t *testing.T) {
 			enumInInput(input: $input)
 		}
 		`, &resp, client.Var("input", map[string]any{"enum": "INVALID"}))
-		require.EqualError(t, err, `http 422: {"errors":[{"message":"INVALID is not a valid EnumTest","path":["variable","input","enum"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
+		require.EqualError(t, err, `http 400: {"errors":[{"message":"INVALID is not a valid EnumTest","path":["variable","input","enum"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
 	})
 }
diff --git a/codegen/testserver/singlefile/input_test.go b/codegen/testserver/singlefile/input_test.go
index 76595275..319df394 100644
--- a/codegen/testserver/singlefile/input_test.go
+++ b/codegen/testserver/singlefile/input_test.go
@@ -30,7 +30,7 @@ func TestInput(t *testing.T) {
 
 		err := c.Post(`query { inputSlice(arg: ["ok", 1, 2, "ok"]) }`, &resp)
 
-		require.EqualError(t, err, `http 422: {"errors":[{"message":"String cannot represent a non string value: 1","locations":[{"line":1,"column":32}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"String cannot represent a non string value: 2","locations":[{"line":1,"column":35}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
+		require.EqualError(t, err, `http 400: {"errors":[{"message":"String cannot represent a non string value: 1","locations":[{"line":1,"column":32}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"String cannot represent a non string value: 2","locations":[{"line":1,"column":35}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`)
 		require.Nil(t, resp.DirectiveArg)
 	})
 
diff --git a/graphql/handler/apollofederatedtracingv1/tracing_test.go b/graphql/handler/apollofederatedtracingv1/tracing_test.go
index f4616bb2..2988e2ca 100644
--- a/graphql/handler/apollofederatedtracingv1/tracing_test.go
+++ b/graphql/handler/apollofederatedtracingv1/tracing_test.go
@@ -127,7 +127,7 @@ func TestApolloTracing_withMissingOp(t *testing.T) {
 	h.Use(&apollofederatedtracingv1.Tracer{})
 
 	resp := doRequest(h, http.MethodPost, "/graphql", `{}`)
-	assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
+	assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
 	b := resp.Body.Bytes()
 	t.Log(string(b))
 	var respData struct {
diff --git a/graphql/handler/server_test.go b/graphql/handler/server_test.go
index ed24072d..b6d84493 100644
--- a/graphql/handler/server_test.go
+++ b/graphql/handler/server_test.go
@@ -106,7 +106,7 @@ func TestServer(t *testing.T) {
 		})
 
 		resp := get(srv, "/foo?query=invalid")
-		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
+		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
 		assert.Len(t, errors1, 1)
 		assert.Len(t, errors2, 1)
 	})
diff --git a/graphql/handler/transport/headers_test.go b/graphql/handler/transport/headers_test.go
index 75876ab5..8ecac753 100644
--- a/graphql/handler/transport/headers_test.go
+++ b/graphql/handler/transport/headers_test.go
@@ -22,10 +22,10 @@ func TestHeadersWithPOST(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.POST{})
 
-		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "application/json")
+		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 1)
-		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
 	})
 
 	t.Run("Headers set", func(t *testing.T) {
@@ -37,7 +37,7 @@ func TestHeadersWithPOST(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.POST{ResponseHeaders: headers})
 
-		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "application/json")
+		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 2)
 		assert.Equal(t, "application/json; charset: utf8", resp.Header().Get("Content-Type"))
@@ -51,10 +51,10 @@ func TestHeadersWithGET(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.GET{})
 
-		resp := doRequest(h, "GET", "/graphql?query={name}", "", "application/json")
+		resp := doRequest(h, "GET", "/graphql?query={name}", "", "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 1)
-		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
 	})
 
 	t.Run("Headers set", func(t *testing.T) {
@@ -66,7 +66,7 @@ func TestHeadersWithGET(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.GET{ResponseHeaders: headers})
 
-		resp := doRequest(h, "GET", "/graphql?query={name}", "", "application/json")
+		resp := doRequest(h, "GET", "/graphql?query={name}", "", "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 2)
 		assert.Equal(t, "application/json; charset: utf8", resp.Header().Get("Content-Type"))
@@ -79,7 +79,7 @@ func TestHeadersWithGRAPHQL(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.GRAPHQL{})
 
-		resp := doRequest(h, "POST", "/graphql", `{ name }`, "application/graphql")
+		resp := doRequest(h, "POST", "/graphql", `{ name }`, "", "application/graphql")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 1)
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
@@ -94,7 +94,7 @@ func TestHeadersWithGRAPHQL(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.GRAPHQL{ResponseHeaders: headers})
 
-		resp := doRequest(h, "POST", "/graphql", `{ name }`, "application/graphql")
+		resp := doRequest(h, "POST", "/graphql", `{ name }`, "", "application/graphql")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 2)
 		assert.Equal(t, "application/json; charset: utf8", resp.Header().Get("Content-Type"))
@@ -107,7 +107,7 @@ func TestHeadersWithFormUrlEncoded(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.UrlEncodedForm{})
 
-		resp := doRequest(h, "POST", "/graphql", `{ name }`, "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", `{ name }`, "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 1)
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
@@ -122,7 +122,7 @@ func TestHeadersWithFormUrlEncoded(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.UrlEncodedForm{ResponseHeaders: headers})
 
-		resp := doRequest(h, "POST", "/graphql", `{ name }`, "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", `{ name }`, "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Len(t, resp.Header(), 2)
 		assert.Equal(t, "application/json; charset: utf8", resp.Header().Get("Content-Type"))
diff --git a/graphql/handler/transport/http_form_urlencode_test.go b/graphql/handler/transport/http_form_urlencode_test.go
index 976d25ee..29644a9a 100644
--- a/graphql/handler/transport/http_form_urlencode_test.go
+++ b/graphql/handler/transport/http_form_urlencode_test.go
@@ -18,39 +18,39 @@ func TestUrlEncodedForm(t *testing.T) {
 	h.AddTransport(transport.UrlEncodedForm{})
 
 	t.Run("success json", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
 	})
 
 	t.Run("success urlencoded", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `query=%7B%20name%20%7D`, "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", `query=%7B%20name%20%7D`, "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
 	})
 
 	t.Run("success plain", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `query={ name }`, "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", `query={ name }`, "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
 	})
 
 	t.Run("decode failure json", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", "notjson", "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", "notjson", "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"Unexpected Name \"notjson\"","locations":[{"line":1,"column":1}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("decode failure urlencoded", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", "query=%7Bnot-good", "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", "query=%7Bnot-good", "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"Expected Name, found \u003cInvalid\u003e","locations":[{"line":1,"column":6}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("parse query failure", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `{"query":{"wrong": "format"}}`, "application/x-www-form-urlencoded")
+		resp := doRequest(h, "POST", "/graphql", `{"query":{"wrong": "format"}}`, "", "application/x-www-form-urlencoded")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"could not cleanup body: json: cannot unmarshal object into Go struct field RawParams.query of type string"}],"data":null}`, resp.Body.String())
diff --git a/graphql/handler/transport/http_get_test.go b/graphql/handler/transport/http_get_test.go
index 36959742..03afa6a5 100644
--- a/graphql/handler/transport/http_get_test.go
+++ b/graphql/handler/transport/http_get_test.go
@@ -14,37 +14,76 @@ func TestGET(t *testing.T) {
 	h := testserver.New()
 	h.AddTransport(transport.GET{})
 
-	t.Run("success", func(t *testing.T) {
-		resp := doRequest(h, "GET", "/graphql?query={name}", ``, "application/json")
+	jsonH := testserver.New()
+	jsonH.AddTransport(transport.GET{
+		ResponseHeaders: map[string][]string{
+			"Content-Type": {"application/json"},
+		},
+	})
+
+	t.Run("success with accept application/json", func(t *testing.T) {
+		resp := doRequest(h, "GET", "/graphql?query={name}", ``, "application/json", "application/json")
+		assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
+	})
+
+	t.Run("success with accept application/graphql-response+json", func(t *testing.T) {
+		resp := doRequest(h, "GET", "/graphql?query={name}", ``, "application/graphql-response+json", "application/json")
+		assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
+	})
+
+	t.Run("success with wildcard", func(t *testing.T) {
+		resp := doRequest(h, "GET", "/graphql?query={name}", ``, "*/*", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
 	})
 
 	t.Run("has json content-type header", func(t *testing.T) {
-		resp := doRequest(h, "GET", "/graphql?query={name}", ``, "application/json")
+		resp := doRequest(h, "GET", "/graphql?query={name}", ``, "application/json", "application/json")
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 	})
 
 	t.Run("decode failure", func(t *testing.T) {
-		resp := doRequest(h, "GET", "/graphql?query={name}&variables=notjson", "", "application/json")
+		resp := doRequest(h, "GET", "/graphql?query={name}&variables=notjson", "", "application/json", "application/json")
 		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"variables could not be decoded"}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("invalid variable", func(t *testing.T) {
-		resp := doRequest(h, "GET", `/graphql?query=query($id:Int!){find(id:$id)}&variables={"id":false}`, "", "application/json")
+		resp := doRequest(h, "GET", `/graphql?query=query($id:Int!){find(id:$id)}&variables={"id":false}`, "", "", "application/json")
+		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"errors":[{"message":"cannot use bool as Int","path":["variable","id"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`, resp.Body.String())
+	})
+
+	t.Run("invalid variable with json only", func(t *testing.T) {
+		resp := doRequest(jsonH, "GET", `/graphql?query=query($id:Int!){find(id:$id)}&variables={"id":false}`, "", "", "application/json")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"cannot use bool as Int","path":["variable","id"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("parse failure", func(t *testing.T) {
-		resp := doRequest(h, "GET", "/graphql?query=!", "", "application/json")
+		resp := doRequest(h, "GET", "/graphql?query=!", "", "", "application/json")
+		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"errors":[{"message":"Unexpected !","locations":[{"line":1,"column":1}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"data":null}`, resp.Body.String())
+	})
+
+	t.Run("parse failure with json only", func(t *testing.T) {
+		resp := doRequest(jsonH, "GET", "/graphql?query=!", "", "", "application/json")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"Unexpected !","locations":[{"line":1,"column":1}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("no mutations", func(t *testing.T) {
-		resp := doRequest(h, "GET", "/graphql?query=mutation{name}", "", "application/json")
+		resp := doRequest(h, "GET", "/graphql?query=mutation{name}", "", "", "application/json")
 		assert.Equal(t, http.StatusNotAcceptable, resp.Code, resp.Body.String())
 		assert.JSONEq(t, `{"errors":[{"message":"GET requests only allow query operations"}],"data":null}`, resp.Body.String())
 	})
diff --git a/graphql/handler/transport/http_post_test.go b/graphql/handler/transport/http_post_test.go
index 99700dd4..e34c6393 100644
--- a/graphql/handler/transport/http_post_test.go
+++ b/graphql/handler/transport/http_post_test.go
@@ -17,42 +17,99 @@ func TestPOST(t *testing.T) {
 	h := testserver.New()
 	h.AddTransport(transport.POST{})
 
-	t.Run("success", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "application/json")
+	jsonH := testserver.New()
+	jsonH.AddTransport(transport.POST{
+		ResponseHeaders: map[string][]string{
+			"Content-Type": {"application/json"},
+		},
+	})
+
+	t.Run("success with accept application/json", func(t *testing.T) {
+		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "application/json", "application/json")
+		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
+	})
+
+	t.Run("success with accept application/graphql-response+json", func(t *testing.T) {
+		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "application/graphql-response+json; charset=utf-8", "application/json")
+		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
+	})
+
+	t.Run("success with accept wildcard", func(t *testing.T) {
+		resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "*/*", "application/json")
+		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
+	})
+
+	t.Run("success with json only", func(t *testing.T) {
+		resp := doRequest(jsonH, "POST", "/graphql", `{"query":"{ name }"}`, "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"data":{"name":"test"}}`, resp.Body.String())
 	})
 
 	t.Run("decode failure", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", "notjson", "application/json")
+		resp := doRequest(h, "POST", "/graphql", "notjson", "application/json", "application/json")
 		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"json request body could not be decoded: invalid character 'o' in literal null (expecting 'u') body:notjson"}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("parse failure", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `{"query": "!"}`, "application/json")
+		resp := doRequest(h, "POST", "/graphql", `{"query": "!"}`, "", "application/json")
+		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"errors":[{"message":"Unexpected !","locations":[{"line":1,"column":1}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"data":null}`, resp.Body.String())
+	})
+
+	t.Run("parse failure with json only", func(t *testing.T) {
+		resp := doRequest(jsonH, "POST", "/graphql", `{"query": "!"}`, "", "application/json")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"Unexpected !","locations":[{"line":1,"column":1}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("validation failure", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `{"query": "{ title }"}`, "application/json")
+		resp := doRequest(h, "POST", "/graphql", `{"query": "{ title }"}`, "", "application/json")
+		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"errors":[{"message":"Cannot query field \"title\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`, resp.Body.String())
+	})
+
+	t.Run("validation failure with json only", func(t *testing.T) {
+		resp := doRequest(jsonH, "POST", "/graphql", `{"query": "{ title }"}`, "", "application/json")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"Cannot query field \"title\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("invalid variable", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `{"query": "query($id:Int!){find(id:$id)}","variables":{"id":false}}`, "application/json")
+		resp := doRequest(h, "POST", "/graphql", `{"query": "query($id:Int!){find(id:$id)}","variables":{"id":false}}`, "", "application/json")
+		assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"errors":[{"message":"cannot use bool as Int","path":["variable","id"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`, resp.Body.String())
+	})
+
+	t.Run("invalid variable with json only", func(t *testing.T) {
+		resp := doRequest(jsonH, "POST", "/graphql", `{"query": "query($id:Int!){find(id:$id)}","variables":{"id":false}}`, "", "application/json")
 		assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"cannot use bool as Int","path":["variable","id"],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}`, resp.Body.String())
 	})
 
 	t.Run("execution failure", func(t *testing.T) {
-		resp := doRequest(h, "POST", "/graphql", `{"query": "mutation { name }"}`, "application/json")
+		resp := doRequest(h, "POST", "/graphql", `{"query": "mutation { name }"}`, "", "application/json")
+		assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
+		assert.Equal(t, "application/graphql-response+json", resp.Header().Get("Content-Type"))
+		assert.JSONEq(t, `{"errors":[{"message":"mutations are not supported"}],"data":null}`, resp.Body.String())
+	})
+
+	t.Run("execution failure with json only", func(t *testing.T) {
+		resp := doRequest(jsonH, "POST", "/graphql", `{"query": "mutation { name }"}`, "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
 		assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
 		assert.JSONEq(t, `{"errors":[{"message":"mutations are not supported"}],"data":null}`, resp.Body.String())
@@ -114,8 +171,11 @@ func TestPOST(t *testing.T) {
 	})
 }
 
-func doRequest(handler http.Handler, method, target, body, contentType string) *httptest.ResponseRecorder {
+func doRequest(handler http.Handler, method, target, body, accept, contentType string) *httptest.ResponseRecorder {
 	r := httptest.NewRequest(method, target, strings.NewReader(body))
+	if accept != "" {
+		r.Header.Set("Accept", accept)
+	}
 	r.Header.Set("Content-Type", contentType)
 	w := httptest.NewRecorder()
 
diff --git a/graphql/handler/transport/options_test.go b/graphql/handler/transport/options_test.go
index f0812c58..116c5ee4 100644
--- a/graphql/handler/transport/options_test.go
+++ b/graphql/handler/transport/options_test.go
@@ -14,7 +14,7 @@ func TestOptions(t *testing.T) {
 	t.Run("responds to options requests with default methods", func(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.Options{})
-		resp := doRequest(h, "OPTIONS", "/graphql?query={me{name}}", ``, "application/json")
+		resp := doRequest(h, "OPTIONS", "/graphql?query={me{name}}", ``, "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Equal(t, "OPTIONS, GET, POST", resp.Header().Get("Allow"))
 	})
@@ -24,7 +24,7 @@ func TestOptions(t *testing.T) {
 		h.AddTransport(transport.Options{
 			AllowedMethods: []string{http.MethodOptions, http.MethodPost, http.MethodHead},
 		})
-		resp := doRequest(h, "OPTIONS", "/graphql?query={me{name}}", ``, "application/json")
+		resp := doRequest(h, "OPTIONS", "/graphql?query={me{name}}", ``, "", "application/json")
 		assert.Equal(t, http.StatusOK, resp.Code)
 		assert.Equal(t, "OPTIONS, POST, HEAD", resp.Header().Get("Allow"))
 	})
@@ -32,7 +32,7 @@ func TestOptions(t *testing.T) {
 	t.Run("responds to head requests", func(t *testing.T) {
 		h := testserver.New()
 		h.AddTransport(transport.Options{})
-		resp := doRequest(h, "HEAD", "/graphql?query={me{name}}", ``, "application/json")
+		resp := doRequest(h, "HEAD", "/graphql?query={me{name}}", ``, "", "application/json")
 		assert.Equal(t, http.StatusMethodNotAllowed, resp.Code)
 	})
 }