-
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/x-www-form-urlencoded content type (#2611)
* Renamed 'form' transport to 'form_multipart'. There are multiple ways form data can be encoded. 'multipart' is just one of them - there are also 'application/x-www-form-urlencoded' (which will be added in next commit) and 'text/plain' encodings. Let each encoding have it's own form_xxxx file and tests. * Adds transport for application/x-www-form-urlencoded content type. This commit adds transport that handles form POST with content type set to 'application/x-www-form-urlencoded'. Form body can be json, urlencoded parameters or plain text. Example: ``` curl -X POST 'http://server/query' -d '{name}' -H "Content-Type: application/x-www-form-urlencoded" ``` Enable it in your GQL server with: ``` srv.AddTransport(transport.UrlEncodedForm{}) ``` * golangci-lint: change ifElseChain to switch. No other changes but this rewrite to switch.
- Loading branch information
Showing
6 changed files
with
236 additions
and
0 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
File renamed without changes.
File renamed without changes.
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,88 @@ | ||
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 TestUrlEncodedForm(t *testing.T) { | ||
h := testserver.New() | ||
h.AddTransport(transport.UrlEncodedForm{}) | ||
|
||
t.Run("success json", func(t *testing.T) { | ||
resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`, "application/x-www-form-urlencoded") | ||
assert.Equal(t, http.StatusOK, resp.Code) | ||
assert.Equal(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") | ||
assert.Equal(t, http.StatusOK, resp.Code) | ||
assert.Equal(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") | ||
assert.Equal(t, http.StatusOK, resp.Code) | ||
assert.Equal(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") | ||
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":"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") | ||
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 \u003cInvalid\u003e","locations":[{"line":1,"column":6}],"extensions":{"code":"GRAPHQL_PARSE_FAILED"}}],"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/x-www-form-urlencoded", | ||
} | ||
|
||
for _, contentType := range validContentTypes { | ||
t.Run(fmt.Sprintf("allow for content type %s", contentType), func(t *testing.T) { | ||
resp := doReq(h, "POST", "/graphql", `{"query":"{ 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()) | ||
}) | ||
} | ||
}) | ||
} |
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,118 @@ | ||
package transport | ||
|
||
import ( | ||
"io" | ||
"log" | ||
"mime" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/vektah/gqlparser/v2/gqlerror" | ||
|
||
"github.com/99designs/gqlgen/graphql" | ||
) | ||
|
||
// FORM implements the application/x-www-form-urlencoded side of the default HTTP transport | ||
type UrlEncodedForm 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 = UrlEncodedForm{} | ||
|
||
func (h UrlEncodedForm) 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/x-www-form-urlencoded" | ||
} | ||
|
||
func (h UrlEncodedForm) 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 form body: %+v", err) | ||
resp := exec.DispatchError(ctx, gqlerror.List{gqlErr}) | ||
log.Printf("could not get json request body: %+v", err.Error()) | ||
writeJson(w, resp) | ||
} | ||
|
||
params, err = h.parseBody(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)) | ||
} | ||
|
||
func (h UrlEncodedForm) parseBody(bodyString string) (*graphql.RawParams, error) { | ||
switch { | ||
case strings.Contains(bodyString, "\"query\":"): | ||
// body is json | ||
return h.parseJson(bodyString) | ||
case strings.HasPrefix(bodyString, "query=%7B"): | ||
// body is urlencoded | ||
return h.parseEncoded(bodyString) | ||
default: | ||
// body is plain text | ||
params := &graphql.RawParams{} | ||
params.Query = strings.TrimPrefix(bodyString, "query=") | ||
|
||
return params, nil | ||
} | ||
} | ||
|
||
func (h UrlEncodedForm) parseEncoded(bodyString string) (*graphql.RawParams, error) { | ||
params := &graphql.RawParams{} | ||
|
||
query, err := url.QueryUnescape(bodyString) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
params.Query = strings.TrimPrefix(query, "query=") | ||
|
||
return params, nil | ||
} | ||
|
||
func (h UrlEncodedForm) parseJson(bodyString string) (*graphql.RawParams, error) { | ||
params := &graphql.RawParams{} | ||
bodyReader := io.NopCloser(strings.NewReader(bodyString)) | ||
|
||
err := jsonDecode(bodyReader, ¶ms) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return params, nil | ||
} |