diff --git a/auth/auth.go b/auth/auth.go index 36729b604aba..58af93188774 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -538,12 +538,12 @@ func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) { v := url.Values{} v.Set("grant_type", defaultGrantType) v.Set("assertion", payload) - resp, err := tp.Client.PostForm(tp.opts.TokenURL, v) + req, err := http.NewRequestWithContext(ctx, "POST", tp.opts.TokenURL, strings.NewReader(v.Encode())) if err != nil { - return nil, fmt.Errorf("auth: cannot fetch token: %w", err) + return nil, err } - defer resp.Body.Close() - body, err := internal.ReadAll(resp.Body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, body, err := internal.DoRequest(tp.Client, req) if err != nil { return nil, fmt.Errorf("auth: cannot fetch token: %w", err) } diff --git a/auth/credentials/downscope/downscope.go b/auth/credentials/downscope/downscope.go index 4ad76c6193d4..0ef479950341 100644 --- a/auth/credentials/downscope/downscope.go +++ b/auth/credentials/downscope/downscope.go @@ -182,21 +182,21 @@ func (dts *downscopedTokenProvider) Token(ctx context.Context) (*auth.Token, err form.Add("subject_token", tok.Value) form.Add("options", string(b)) - resp, err := dts.Client.PostForm(dts.identityBindingEndpoint, form) + req, err := http.NewRequestWithContext(ctx, "POST", dts.identityBindingEndpoint, strings.NewReader(form.Encode())) if err != nil { return nil, err } - defer resp.Body.Close() - respBody, err := internal.ReadAll(resp.Body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, body, err := internal.DoRequest(dts.Client, req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("downscope: unable to exchange token, %v: %s", resp.StatusCode, respBody) + return nil, fmt.Errorf("downscope: unable to exchange token, %v: %s", resp.StatusCode, body) } var tresp downscopedTokenResponse - err = json.Unmarshal(respBody, &tresp) + err = json.Unmarshal(body, &tresp) if err != nil { return nil, err } diff --git a/auth/credentials/idtoken/cache.go b/auth/credentials/idtoken/cache.go index 6eb6d3b4445f..e6f4ff811608 100644 --- a/auth/credentials/idtoken/cache.go +++ b/auth/credentials/idtoken/cache.go @@ -23,6 +23,8 @@ import ( "strings" "sync" "time" + + "cloud.google.com/go/auth/internal" ) type cachingClient struct { @@ -52,22 +54,20 @@ func (c *cachingClient) getCert(ctx context.Context, url string) (*certResponse, if response, ok := c.get(url); ok { return response, nil } - req, err := http.NewRequest(http.MethodGet, url, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } - req = req.WithContext(ctx) - resp, err := c.client.Do(req) + resp, body, err := internal.DoRequest(c.client, req) if err != nil { return nil, err } - defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("idtoken: unable to retrieve cert, got status code %d", resp.StatusCode) } certResp := &certResponse{} - if err := json.NewDecoder(resp.Body).Decode(certResp); err != nil { + if err := json.Unmarshal(body, &certResp); err != nil { return nil, err } diff --git a/auth/credentials/idtoken/compute.go b/auth/credentials/idtoken/compute.go index d6757b60f871..fb9c62c610dc 100644 --- a/auth/credentials/idtoken/compute.go +++ b/auth/credentials/idtoken/compute.go @@ -66,7 +66,7 @@ func (c computeIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) v.Set("licenses", "TRUE") } urlSuffix := identitySuffix + "?" + v.Encode() - res, err := c.client.Get(urlSuffix) + res, err := c.client.GetWithContext(ctx, urlSuffix) if err != nil { return nil, err } diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index 95a4c492ebf6..d4affc173363 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -162,20 +162,15 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er } url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal)) - req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) if err != nil { return nil, fmt.Errorf("impersonate: unable to create request: %w", err) } req.Header.Set("Content-Type", "application/json") - resp, err := i.client.Do(req) + resp, body, err := internal.DoRequest(i.client, req) if err != nil { return nil, fmt.Errorf("impersonate: unable to generate ID token: %w", err) } - defer resp.Body.Close() - body, err := internal.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to read body: %w", err) - } if c := resp.StatusCode; c < 200 || c > 299 { return nil, fmt.Errorf("impersonate: status code %d: %s", c, body) } diff --git a/auth/credentials/impersonate/impersonate.go b/auth/credentials/impersonate/impersonate.go index a0045db45fd1..0be955acde8e 100644 --- a/auth/credentials/impersonate/impersonate.go +++ b/auth/credentials/impersonate/impersonate.go @@ -238,21 +238,15 @@ func (i impersonatedTokenProvider) Token(ctx context.Context) (*auth.Token, erro return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) } url := fmt.Sprintf("%s/v1/%s:generateAccessToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal)) - req, err := http.NewRequest("POST", url, bytes.NewReader(b)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("impersonate: unable to create request: %w", err) } req.Header.Set("Content-Type", "application/json") - - resp, err := i.client.Do(req) + resp, body, err := internal.DoRequest(i.client, req) if err != nil { return nil, fmt.Errorf("impersonate: unable to generate access token: %w", err) } - defer resp.Body.Close() - body, err := internal.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to read body: %w", err) - } if c := resp.StatusCode; c < 200 || c > 299 { return nil, fmt.Errorf("impersonate: status code %d: %s", c, body) } diff --git a/auth/credentials/impersonate/user.go b/auth/credentials/impersonate/user.go index 5aefa2a8e301..1acaaa922d9d 100644 --- a/auth/credentials/impersonate/user.go +++ b/auth/credentials/impersonate/user.go @@ -92,14 +92,14 @@ type userTokenProvider struct { } func (u userTokenProvider) Token(ctx context.Context) (*auth.Token, error) { - signedJWT, err := u.signJWT() + signedJWT, err := u.signJWT(ctx) if err != nil { return nil, err } return u.exchangeToken(ctx, signedJWT) } -func (u userTokenProvider) signJWT() (string, error) { +func (u userTokenProvider) signJWT(ctx context.Context) (string, error) { now := time.Now() exp := now.Add(u.lifetime) claims := claimSet{ @@ -124,20 +124,16 @@ func (u userTokenProvider) signJWT() (string, error) { return "", fmt.Errorf("impersonate: unable to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, formatIAMServiceAccountName(u.targetPrincipal)) - req, err := http.NewRequest("POST", reqURL, bytes.NewReader(bodyBytes)) + req, err := http.NewRequestWithContext(ctx, "POST", reqURL, bytes.NewReader(bodyBytes)) if err != nil { return "", fmt.Errorf("impersonate: unable to create request: %w", err) } req.Header.Set("Content-Type", "application/json") - rawResp, err := u.client.Do(req) + resp, body, err := internal.DoRequest(u.client, req) if err != nil { return "", fmt.Errorf("impersonate: unable to sign JWT: %w", err) } - body, err := internal.ReadAll(rawResp.Body) - if err != nil { - return "", fmt.Errorf("impersonate: unable to read body: %w", err) - } - if c := rawResp.StatusCode; c < 200 || c > 299 { + if c := resp.StatusCode; c < 200 || c > 299 { return "", fmt.Errorf("impersonate: status code %d: %s", c, body) } @@ -157,15 +153,11 @@ func (u userTokenProvider) exchangeToken(ctx context.Context, signedJWT string) if err != nil { return nil, err } - rawResp, err := u.client.Do(req) + resp, body, err := internal.DoRequest(u.client, req) if err != nil { return nil, fmt.Errorf("impersonate: unable to exchange token: %w", err) } - body, err := internal.ReadAll(rawResp.Body) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to read body: %w", err) - } - if c := rawResp.StatusCode; c < 200 || c > 299 { + if c := resp.StatusCode; c < 200 || c > 299 { return nil, fmt.Errorf("impersonate: status code %d: %s", c, body) } diff --git a/auth/credentials/internal/externalaccount/aws_provider.go b/auth/credentials/internal/externalaccount/aws_provider.go index d9e1dcddf64d..a34f6b06f846 100644 --- a/auth/credentials/internal/externalaccount/aws_provider.go +++ b/auth/credentials/internal/externalaccount/aws_provider.go @@ -122,7 +122,7 @@ func (sp *awsSubjectProvider) subjectToken(ctx context.Context) (string, error) // Generate the signed request to AWS STS GetCallerIdentity API. // Use the required regional endpoint. Otherwise, the request will fail. - req, err := http.NewRequest("POST", strings.Replace(sp.RegionalCredVerificationURL, "{region}", sp.region, 1), nil) + req, err := http.NewRequestWithContext(ctx, "POST", strings.Replace(sp.RegionalCredVerificationURL, "{region}", sp.region, 1), nil) if err != nil { return "", err } @@ -194,20 +194,14 @@ func (sp *awsSubjectProvider) getAWSSessionToken(ctx context.Context) (string, e } req.Header.Set(awsIMDSv2SessionTTLHeader, awsIMDSv2SessionTTL) - resp, err := sp.Client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - respBody, err := internal.ReadAll(resp.Body) + resp, body, err := internal.DoRequest(sp.Client, req) if err != nil { return "", err } if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("credentials: unable to retrieve AWS session token: %s", respBody) + return "", fmt.Errorf("credentials: unable to retrieve AWS session token: %s", body) } - return string(respBody), nil + return string(body), nil } func (sp *awsSubjectProvider) getRegion(ctx context.Context, headers map[string]string) (string, error) { @@ -233,29 +227,21 @@ func (sp *awsSubjectProvider) getRegion(ctx context.Context, headers map[string] for name, value := range headers { req.Header.Add(name, value) } - - resp, err := sp.Client.Do(req) + resp, body, err := internal.DoRequest(sp.Client, req) if err != nil { return "", err } - defer resp.Body.Close() - - respBody, err := internal.ReadAll(resp.Body) - if err != nil { - return "", err - } - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("credentials: unable to retrieve AWS region - %s", respBody) + return "", fmt.Errorf("credentials: unable to retrieve AWS region - %s", body) } // This endpoint will return the region in format: us-east-2b. // Only the us-east-2 part should be used. - bodyLen := len(respBody) + bodyLen := len(body) if bodyLen == 0 { return "", nil } - return string(respBody[:bodyLen-1]), nil + return string(body[:bodyLen-1]), nil } func (sp *awsSubjectProvider) getSecurityCredentials(ctx context.Context, headers map[string]string) (result *AwsSecurityCredentials, err error) { @@ -299,22 +285,17 @@ func (sp *awsSubjectProvider) getMetadataSecurityCredentials(ctx context.Context for name, value := range headers { req.Header.Add(name, value) } - - resp, err := sp.Client.Do(req) - if err != nil { - return result, err - } - defer resp.Body.Close() - - respBody, err := internal.ReadAll(resp.Body) + resp, body, err := internal.DoRequest(sp.Client, req) if err != nil { return result, err } if resp.StatusCode != http.StatusOK { - return result, fmt.Errorf("credentials: unable to retrieve AWS security credentials - %s", respBody) + return result, fmt.Errorf("credentials: unable to retrieve AWS security credentials - %s", body) + } + if err := json.Unmarshal(body, &result); err != nil { + return nil, err } - err = json.Unmarshal(respBody, &result) - return result, err + return result, nil } func (sp *awsSubjectProvider) getMetadataRoleName(ctx context.Context, headers map[string]string) (string, error) { @@ -329,20 +310,14 @@ func (sp *awsSubjectProvider) getMetadataRoleName(ctx context.Context, headers m req.Header.Add(name, value) } - resp, err := sp.Client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - respBody, err := internal.ReadAll(resp.Body) + resp, body, err := internal.DoRequest(sp.Client, req) if err != nil { return "", err } if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("credentials: unable to retrieve AWS role name - %s", respBody) + return "", fmt.Errorf("credentials: unable to retrieve AWS role name - %s", body) } - return string(respBody), nil + return string(body), nil } // awsRequestSigner is a utility class to sign http requests using a AWS V4 signature. diff --git a/auth/credentials/internal/externalaccount/url_provider.go b/auth/credentials/internal/externalaccount/url_provider.go index 22b8af1c11b8..e33d35a2687f 100644 --- a/auth/credentials/internal/externalaccount/url_provider.go +++ b/auth/credentials/internal/externalaccount/url_provider.go @@ -48,27 +48,21 @@ func (sp *urlSubjectProvider) subjectToken(ctx context.Context) (string, error) for key, val := range sp.Headers { req.Header.Add(key, val) } - resp, err := sp.Client.Do(req) + resp, body, err := internal.DoRequest(sp.Client, req) if err != nil { return "", fmt.Errorf("credentials: invalid response when retrieving subject token: %w", err) } - defer resp.Body.Close() - - respBody, err := internal.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("credentials: invalid body in subject token URL query: %w", err) - } if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices { - return "", fmt.Errorf("credentials: status code %d: %s", c, respBody) + return "", fmt.Errorf("credentials: status code %d: %s", c, body) } if sp.Format == nil { - return string(respBody), nil + return string(body), nil } switch sp.Format.Type { case "json": jsonData := make(map[string]interface{}) - err = json.Unmarshal(respBody, &jsonData) + err = json.Unmarshal(body, &jsonData) if err != nil { return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err) } @@ -82,7 +76,7 @@ func (sp *urlSubjectProvider) subjectToken(ctx context.Context) (string, error) } return token, nil case fileTypeText: - return string(respBody), nil + return string(body), nil default: return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type) } diff --git a/auth/credentials/internal/gdch/gdch.go b/auth/credentials/internal/gdch/gdch.go index 467edb9088e4..720045d3b072 100644 --- a/auth/credentials/internal/gdch/gdch.go +++ b/auth/credentials/internal/gdch/gdch.go @@ -25,6 +25,7 @@ import ( "net/http" "net/url" "os" + "strings" "time" "cloud.google.com/go/auth" @@ -129,12 +130,13 @@ func (g gdchProvider) Token(ctx context.Context) (*auth.Token, error) { v.Set("requested_token_type", requestTokenType) v.Set("subject_token", payload) v.Set("subject_token_type", subjectTokenType) - resp, err := g.client.PostForm(g.tokenURL, v) + + req, err := http.NewRequestWithContext(ctx, "POST", g.tokenURL, strings.NewReader(v.Encode())) if err != nil { - return nil, fmt.Errorf("credentials: cannot fetch token: %w", err) + return nil, err } - defer resp.Body.Close() - body, err := internal.ReadAll(resp.Body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, body, err := internal.DoRequest(g.client, req) if err != nil { return nil, fmt.Errorf("credentials: cannot fetch token: %w", err) } diff --git a/auth/credentials/internal/impersonate/impersonate.go b/auth/credentials/internal/impersonate/impersonate.go index 3ceab873b8e4..ed53afa519e7 100644 --- a/auth/credentials/internal/impersonate/impersonate.go +++ b/auth/credentials/internal/impersonate/impersonate.go @@ -109,15 +109,10 @@ func (o *Options) Token(ctx context.Context) (*auth.Token, error) { if err := setAuthHeader(ctx, o.Tp, req); err != nil { return nil, err } - resp, err := o.Client.Do(req) + resp, body, err := internal.DoRequest(o.Client, req) if err != nil { return nil, fmt.Errorf("credentials: unable to generate access token: %w", err) } - defer resp.Body.Close() - body, err := internal.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("credentials: unable to read body: %w", err) - } if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices { return nil, fmt.Errorf("credentials: status code %d: %s", c, body) } diff --git a/auth/credentials/internal/stsexchange/sts_exchange.go b/auth/credentials/internal/stsexchange/sts_exchange.go index f70e0aef48fe..768a9dafc13a 100644 --- a/auth/credentials/internal/stsexchange/sts_exchange.go +++ b/auth/credentials/internal/stsexchange/sts_exchange.go @@ -93,16 +93,10 @@ func doRequest(ctx context.Context, opts *Options, data url.Values) (*TokenRespo } req.Header.Set("Content-Length", strconv.Itoa(len(encodedData))) - resp, err := opts.Client.Do(req) + resp, body, err := internal.DoRequest(opts.Client, req) if err != nil { return nil, fmt.Errorf("credentials: invalid response from Secure Token Server: %w", err) } - defer resp.Body.Close() - - body, err := internal.ReadAll(resp.Body) - if err != nil { - return nil, err - } if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices { return nil, fmt.Errorf("credentials: status code %d: %s", c, body) } diff --git a/auth/internal/internal.go b/auth/internal/internal.go index 70534e809a4a..8c328e2fbd9c 100644 --- a/auth/internal/internal.go +++ b/auth/internal/internal.go @@ -124,6 +124,21 @@ func GetProjectID(b []byte, override string) string { return v.Project } +// DoRequest executes the provided req with the client. It reads the response +// body, closes it, and returns it. +func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) { + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize)) + if err != nil { + return nil, nil, err + } + return resp, body, nil +} + // ReadAll consumes the whole reader and safely reads the content of its body // with some overflow protection. func ReadAll(r io.Reader) ([]byte, error) { @@ -167,8 +182,7 @@ func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string // httpGetMetadataUniverseDomain is a package var for unit test substitution. var httpGetMetadataUniverseDomain = func(ctx context.Context) (string, error) { client := metadata.NewClient(&http.Client{Timeout: time.Second}) - // TODO(quartzmo): set ctx on request - return client.Get("universe/universe_domain") + return client.GetWithContext(ctx, "universe/universe_domain") } func getMetadataUniverseDomain(ctx context.Context) (string, error) { diff --git a/auth/internal/testutil/testdns/dns.go b/auth/internal/testutil/testdns/dns.go index 256216dceb42..7634aebe8fae 100644 --- a/auth/internal/testutil/testdns/dns.go +++ b/auth/internal/testutil/testdns/dns.go @@ -46,7 +46,7 @@ func NewClient(tp auth.TokenProvider) *Client { // GetProject calls the GET project endpoint. func (c *Client) GetProject(ctx context.Context, projectID string) error { - req, err := http.NewRequest("GET", fmt.Sprintf("https://dns.googleapis.com/dns/v1/projects/%s", projectID), nil) + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://dns.googleapis.com/dns/v1/projects/%s", projectID), nil) if err != nil { return err } diff --git a/auth/internal/testutil/testgcs/storage.go b/auth/internal/testutil/testgcs/storage.go index e61853923ea4..b9ef65b7669a 100644 --- a/auth/internal/testutil/testgcs/storage.go +++ b/auth/internal/testutil/testgcs/storage.go @@ -57,7 +57,7 @@ func (c *Client) CreateBucket(ctx context.Context, projectID, bucket string) err return err } - req, err := http.NewRequest("POST", fmt.Sprintf("https://storage.googleapis.com/storage/v1/b?project=%s", projectID), bytes.NewReader(b)) + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("https://storage.googleapis.com/storage/v1/b?project=%s", projectID), bytes.NewReader(b)) if err != nil { return err } @@ -79,7 +79,7 @@ func (c *Client) CreateBucket(ctx context.Context, projectID, bucket string) err // DeleteBucket deletes the specified bucket. func (c *Client) DeleteBucket(ctx context.Context, bucket string) error { - req, err := http.NewRequest("DELETE", fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s", bucket), nil) + req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s", bucket), nil) if err != nil { return err } @@ -101,7 +101,7 @@ func (c *Client) DeleteBucket(ctx context.Context, bucket string) error { // DownloadObject returns an [http.Response] who's body can be consumed to // read the contents of an object. func (c *Client) DownloadObject(ctx context.Context, bucket, object string) (*http.Response, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o/%s", bucket, object), nil) + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o/%s", bucket, object), nil) if err != nil { return nil, err } diff --git a/auth/threelegged.go b/auth/threelegged.go index 1ccdeff84d0f..a8ce6cd8a8db 100644 --- a/auth/threelegged.go +++ b/auth/threelegged.go @@ -285,7 +285,7 @@ func fetchToken(ctx context.Context, o *Options3LO, v url.Values) (*Token, strin v.Set("client_secret", o.ClientSecret) } } - req, err := http.NewRequest("POST", o.TokenURL, strings.NewReader(v.Encode())) + req, err := http.NewRequestWithContext(ctx, "POST", o.TokenURL, strings.NewReader(v.Encode())) if err != nil { return nil, refreshToken, err } @@ -295,25 +295,19 @@ func fetchToken(ctx context.Context, o *Options3LO, v url.Values) (*Token, strin } // Make request - r, err := o.client().Do(req.WithContext(ctx)) + resp, body, err := internal.DoRequest(o.client(), req) if err != nil { return nil, refreshToken, err } - body, err := internal.ReadAll(r.Body) - r.Body.Close() - if err != nil { - return nil, refreshToken, fmt.Errorf("auth: cannot fetch token: %w", err) - } - - failureStatus := r.StatusCode < 200 || r.StatusCode > 299 + failureStatus := resp.StatusCode < 200 || resp.StatusCode > 299 tokError := &Error{ - Response: r, + Response: resp, Body: body, } var token *Token // errors ignored because of default switch on content - content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + content, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) switch content { case "application/x-www-form-urlencoded", "text/plain": // some endpoints return a query string