-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Transport for application/graphql contentType (#2592)
* Adds application/graphql transport layer This commit adds 'application/graphql' transport. It is based on POST metod and has only the 'query' part in it's body. See: https://graphql.org/learn/serving-over-http/#post-request and it's comment about this content-type. An example of correct application/graphql query is: ``` curl 'http://host/graphql' -d '{time{now}}' -H "Content-Type: application/graphql" ``` Some clients prefix body with 'query=': ``` -d 'query={time{now}}' ``` Some clients html encode body payload: ``` -d 'query=%7Btime%7Bnow%7D%7D' ``` We cleanup both in cleanupBody() method. Tests are in http_graphql_test.go file. * Adds tests for GRAPHQL transport response headers. GRAPHQL transport (like GET, POST and MULTIPART transports) can have specific response headers added. This commit adds tests for it and changes doRequest() method so that we can set inbound Content-Type. Graphql transport uses 'application/graphql' content-type and not 'application/json'. * Adds GRAPHQL transfer to the documentation.
- Loading branch information
Showing
7 changed files
with
248 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package transport | ||
|
||
import ( | ||
"log" | ||
"mime" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/vektah/gqlparser/v2/gqlerror" | ||
|
||
"github.com/99designs/gqlgen/graphql" | ||
) | ||
|
||
// GRAPHQL implements the application/graphql side of the HTTP transport | ||
// see: https://graphql.org/learn/serving-over-http/#post-request | ||
// If the "application/graphql" Content-Type header is present, treat | ||
// the HTTP POST body contents as the GraphQL query string. | ||
type GRAPHQL struct { | ||
// Map of all headers that are added to graphql response. If not | ||
// set, only one header: Content-Type: application/json will be set. | ||
ResponseHeaders map[string][]string | ||
} | ||
|
||
var _ graphql.Transport = GRAPHQL{} | ||
|
||
func (h GRAPHQL) Supports(r *http.Request) bool { | ||
if r.Header.Get("Upgrade") != "" { | ||
return false | ||
} | ||
|
||
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) | ||
if err != nil { | ||
return false | ||
} | ||
|
||
return r.Method == "POST" && mediaType == "application/graphql" | ||
} | ||
|
||
func (h GRAPHQL) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) { | ||
ctx := r.Context() | ||
writeHeaders(w, h.ResponseHeaders) | ||
params := &graphql.RawParams{} | ||
start := graphql.Now() | ||
params.Headers = r.Header | ||
params.ReadTime = graphql.TraceTiming{ | ||
Start: start, | ||
End: graphql.Now(), | ||
} | ||
|
||
bodyString, err := getRequestBody(r) | ||
if err != nil { | ||
gqlErr := gqlerror.Errorf("could not get request body: %+v", err) | ||
resp := exec.DispatchError(ctx, gqlerror.List{gqlErr}) | ||
log.Printf("could not get request body: %+v", err.Error()) | ||
writeJson(w, resp) | ||
} | ||
|
||
params.Query, err = cleanupBody(bodyString) | ||
if err != nil { | ||
gqlErr := gqlerror.Errorf("could not cleanup body: %+v", err) | ||
resp := exec.DispatchError(ctx, gqlerror.List{gqlErr}) | ||
log.Printf("could not cleanup body: %+v", err.Error()) | ||
writeJson(w, resp) | ||
} | ||
|
||
rc, OpErr := exec.CreateOperationContext(ctx, params) | ||
if OpErr != nil { | ||
w.WriteHeader(statusFor(OpErr)) | ||
resp := exec.DispatchError(graphql.WithOperationContext(ctx, rc), OpErr) | ||
writeJson(w, resp) | ||
return | ||
} | ||
|
||
var responses graphql.ResponseHandler | ||
responses, ctx = exec.DispatchOperation(ctx, rc) | ||
writeJson(w, responses(ctx)) | ||
} | ||
|
||
// Makes sure we strip "query=" keyword from body and | ||
// that body is not url escaped | ||
func cleanupBody(body string) (out string, err error) { | ||
// Some clients send 'query=' at the start of body payload. Let's remove | ||
// it to get GQL query only. | ||
body = strings.TrimPrefix(body, "query=") | ||
|
||
// Body payload can be url encoded or not. We check if %7B - "{" character | ||
// is where query starts. If it is, query is url encoded. | ||
if strings.HasPrefix(body, "%7B") { | ||
body, err = url.QueryUnescape(body) | ||
|
||
if err != nil { | ||
return body, err | ||
} | ||
} | ||
|
||
return body, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package transport_test | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/99designs/gqlgen/graphql/handler/testserver" | ||
"github.com/99designs/gqlgen/graphql/handler/transport" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestGRAPHQL(t *testing.T) { | ||
h := testserver.New() | ||
h.AddTransport(transport.GRAPHQL{}) | ||
|
||
t.Run("success", func(t *testing.T) { | ||
resp := doGraphqlRequest(h, "POST", "/graphql", `{ name }`) | ||
assert.Equal(t, http.StatusOK, resp.Code) | ||
assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) | ||
}) | ||
|
||
t.Run("success even if url encoded", func(t *testing.T) { | ||
resp := doGraphqlRequest(h, "POST", "/graphql", `%7B%20name%20%7D`) | ||
assert.Equal(t, http.StatusOK, resp.Code) | ||
assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) | ||
}) | ||
|
||
t.Run("parse failure", func(t *testing.T) { | ||
resp := doGraphqlRequest(h, "POST", "/graphql", `{"!"}`) | ||
assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String()) | ||
assert.Equal(t, resp.Header().Get("Content-Type"), "application/json") | ||
assert.Equal(t, `{"errors":[{"message":"Expected Name, found String","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"data":null}`, resp.Body.String()) | ||
}) | ||
|
||
t.Run("validation failure", func(t *testing.T) { | ||
resp := doGraphqlRequest(h, "POST", "/graphql", `{ title }`) | ||
assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String()) | ||
assert.Equal(t, resp.Header().Get("Content-Type"), "application/json") | ||
assert.Equal(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("execution failure", func(t *testing.T) { | ||
resp := doGraphqlRequest(h, "POST", "/graphql", `mutation { name }`) | ||
assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String()) | ||
assert.Equal(t, resp.Header().Get("Content-Type"), "application/json") | ||
assert.Equal(t, `{"errors":[{"message":"mutations are not supported"}],"data":null}`, resp.Body.String()) | ||
}) | ||
|
||
t.Run("validate content type", func(t *testing.T) { | ||
doReq := func(handler http.Handler, method string, target string, body string, contentType string) *httptest.ResponseRecorder { | ||
r := httptest.NewRequest(method, target, strings.NewReader(body)) | ||
if contentType != "" { | ||
r.Header.Set("Content-Type", contentType) | ||
} | ||
w := httptest.NewRecorder() | ||
|
||
handler.ServeHTTP(w, r) | ||
return w | ||
} | ||
|
||
validContentTypes := []string{ | ||
"application/graphql", | ||
"application/graphql; charset=utf-8", | ||
} | ||
|
||
for _, contentType := range validContentTypes { | ||
t.Run(fmt.Sprintf("allow for content type %s", contentType), func(t *testing.T) { | ||
resp := doReq(h, "POST", "/graphql", `{ name }`, contentType) | ||
assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String()) | ||
assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) | ||
}) | ||
} | ||
|
||
invalidContentTypes := []string{ | ||
"", | ||
"text/plain", | ||
} | ||
|
||
for _, tc := range invalidContentTypes { | ||
t.Run(fmt.Sprintf("reject for content type %s", tc), func(t *testing.T) { | ||
resp := doReq(h, "POST", "/graphql", `{"query":"{ name }"}`, tc) | ||
assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String()) | ||
assert.Equal(t, fmt.Sprintf(`{"errors":[{"message":"%s"}],"data":null}`, "transport not supported"), resp.Body.String()) | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
func doGraphqlRequest(handler http.Handler, method string, target string, body string) *httptest.ResponseRecorder { | ||
r := httptest.NewRequest(method, target, strings.NewReader(body)) | ||
r.Header.Set("Content-Type", "application/graphql") | ||
w := httptest.NewRecorder() | ||
|
||
handler.ServeHTTP(w, r) | ||
return w | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.