Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(httpclient): support appending the same header multiple times #830

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions backend/http_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package backend
import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
Expand All @@ -16,7 +17,7 @@ type HTTPSettings struct {
BasicAuthEnabled bool
BasicAuthUser string
BasicAuthPassword string
Headers map[string]string
Header http.Header

Timeout time.Duration
DialTimeout time.Duration
Expand Down Expand Up @@ -52,7 +53,7 @@ type HTTPSettings struct {
// HTTPClientOptions creates and returns httpclient.Options.
func (s *HTTPSettings) HTTPClientOptions() httpclient.Options {
opts := httpclient.Options{
Headers: s.Headers,
Header: s.Header,
Labels: map[string]string{},
CustomOptions: map[string]interface{}{},
}
Expand Down Expand Up @@ -104,7 +105,7 @@ func (s *HTTPSettings) HTTPClientOptions() httpclient.Options {
//gocyclo:ignore
func parseHTTPSettings(jsonData json.RawMessage, secureJSONData map[string]string) (*HTTPSettings, error) {
s := &HTTPSettings{
Headers: map[string]string{},
Header: http.Header{},
}

var dat map[string]interface{}
Expand Down Expand Up @@ -273,7 +274,7 @@ func parseHTTPSettings(jsonData json.RawMessage, secureJSONData map[string]strin

if key, exists := dat[headerNameSuffix]; exists {
if value, exists := secureJSONData[headerValueSuffix]; exists {
s.Headers[key.(string)] = value
s.Header.Add(key.(string), value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't understand why key needs to be cast to a string - isn't it already a string since dat is map[string]interface{}?

Copy link
Contributor Author

@sathieu sathieu Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dat is map[string]interface{}, so key := dat[headerNameSuffix] is interface{}.

On the other side secureJSONData is map[string]string, so value doesn't need cast.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah I see that now 👍

}
} else {
// No (more) header values are available
Expand Down
17 changes: 10 additions & 7 deletions backend/http_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backend

import (
"encoding/json"
"net/http"
"testing"
"time"

Expand Down Expand Up @@ -44,7 +45,8 @@ func TestParseHTTPSettings(t *testing.T) {
"sigV4ExternalId": "ext123",
"sigV4Profile": "ghi",
"httpHeaderName1": "X-HeaderOne",
"httpHeaderName2": "X-HeaderTwo"
"httpHeaderName2": "X-HeaderTwo",
"httpHeaderName3": "X-HeaderTwo"
}`
secureData := map[string]string{
"basicAuthPassword": "pwd",
Expand All @@ -55,6 +57,7 @@ func TestParseHTTPSettings(t *testing.T) {
"sigV4SecretKey": "sigV4SecretKey5",
"httpHeaderValue1": "SecretOne",
"httpHeaderValue2": "SecretTwo",
"httpHeaderValue3": "SecretThree",
}
var jsonMap map[string]interface{}
err := json.Unmarshal([]byte(jsonStr), &jsonMap)
Expand All @@ -68,9 +71,9 @@ func TestParseHTTPSettings(t *testing.T) {
BasicAuthEnabled: true,
BasicAuthUser: "user",
BasicAuthPassword: "pwd",
Headers: map[string]string{
"X-HeaderOne": "SecretOne",
"X-HeaderTwo": "SecretTwo",
Header: http.Header{
"X-Headerone": {"SecretOne"},
"X-Headertwo": {"SecretTwo", "SecretThree"},
},
Timeout: 10 * time.Second,
DialTimeout: 10 * time.Second,
Expand Down Expand Up @@ -108,9 +111,9 @@ func TestParseHTTPSettings(t *testing.T) {
User: "user",
Password: "pwd",
},
Headers: map[string]string{
"X-HeaderOne": "SecretOne",
"X-HeaderTwo": "SecretTwo",
Header: http.Header{
"X-Headerone": {"SecretOne"},
"X-Headertwo": {"SecretTwo", "SecretThree"},
},
Timeouts: &httpclient.TimeoutOptions{
Timeout: 10 * time.Second,
Expand Down
10 changes: 6 additions & 4 deletions backend/httpclient/custom_headers_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ const CustomHeadersMiddlewareName = "CustomHeaders"
// If opts.Headers is empty, next will be returned.
func CustomHeadersMiddleware() Middleware {
return NamedMiddlewareFunc(CustomHeadersMiddlewareName, func(opts Options, next http.RoundTripper) http.RoundTripper {
if len(opts.Headers) == 0 {
if len(opts.Header) == 0 {
return next
}

return RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
for key, value := range opts.Headers {
for key, values := range opts.Header {
// According to https://pkg.go.dev/net/http#Request.Header, Host is a special case
if http.CanonicalHeaderKey(key) == "Host" {
req.Host = value
req.Host = values[0]
} else {
req.Header.Set(key, value)
for _, value := range values {
req.Header.Add(key, value)
}
}
}

Expand Down
18 changes: 9 additions & 9 deletions backend/httpclient/custom_headers_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ func TestCustomHeadersMiddleware(t *testing.T) {
ctx := &testContext{}
finalRoundTripper := ctx.createRoundTripper("final")
customHeaders := CustomHeadersMiddleware()
rt := customHeaders.CreateMiddleware(Options{Headers: map[string]string{
"X-HeaderOne": "ValueOne",
"X-HeaderTwo": "ValueTwo",
"X-HeaderThree": "ValueThree",
rt := customHeaders.CreateMiddleware(Options{Header: http.Header{
"X-Headerone": {"ValueOne"},
"X-Headertwo": {"ValueTwo"},
"X-Headerthree": {"ValueThree", "ValueThreeAgain"},
}}, finalRoundTripper)
require.NotNil(t, rt)
middlewareName, ok := customHeaders.(MiddlewareName)
Expand All @@ -55,17 +55,17 @@ func TestCustomHeadersMiddleware(t *testing.T) {
require.Len(t, ctx.callChain, 1)
require.ElementsMatch(t, []string{"final"}, ctx.callChain)

require.Equal(t, "ValueOne", req.Header.Get("X-HeaderOne"))
require.Equal(t, "ValueTwo", req.Header.Get("X-HeaderTwo"))
require.Equal(t, "ValueThree", req.Header.Get("X-HeaderThree"))
require.Equal(t, []string{"ValueOne"}, req.Header.Values("X-Headerone"))
require.Equal(t, []string{"ValueTwo"}, req.Header.Values("X-Headertwo"))
require.Equal(t, []string{"ValueThree", "ValueThreeAgain"}, req.Header.Values("X-Headerthree"))
})

t.Run("With custom Host header set should apply Host to the request", func(t *testing.T) {
ctx := &testContext{}
finalRoundTripper := ctx.createRoundTripper("final")
customHeaders := CustomHeadersMiddleware()
rt := customHeaders.CreateMiddleware(Options{Headers: map[string]string{
"Host": "example.com",
rt := customHeaders.CreateMiddleware(Options{Header: http.Header{
"Host": {"example.com"},
}}, finalRoundTripper)
require.NotNil(t, rt)
middlewareName, ok := customHeaders.(MiddlewareName)
Expand Down
2 changes: 1 addition & 1 deletion backend/httpclient/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Options struct {
ProxyOptions *proxy.Options

// Headers custom headers.
Headers map[string]string
Header http.Header

// CustomOptions allows custom options to be provided.
CustomOptions map[string]interface{}
Expand Down
10 changes: 5 additions & 5 deletions experimental/authclient/authclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestNew(t *testing.T) {
t.Run("client credentials", func(t *testing.T) {
t.Run("valid client credentials", func(t *testing.T) {
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
Header: http.Header{"H1": {"v1"}},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
OAuth2Options: &authclient.OAuth2Options{
Expand All @@ -54,7 +54,7 @@ func TestNew(t *testing.T) {
})
t.Run("valid client credentials with basic auth settings", func(t *testing.T) {
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
Header: http.Header{"H1": {"v1"}},
BasicAuth: &httpclient.BasicAuthOptions{User: "userFoo", Password: "pwdBar"},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
Expand All @@ -81,7 +81,7 @@ func TestNew(t *testing.T) {
t.Run("invalid private key", func(t *testing.T) {
privateKey := generateKey(t, true)
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
Header: http.Header{"H1": {"v1"}},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
OAuth2Options: &authclient.OAuth2Options{
Expand All @@ -103,7 +103,7 @@ func TestNew(t *testing.T) {
t.Run("valid private key", func(t *testing.T) {
privateKey := generateKey(t, false)
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
Header: http.Header{"H1": {"v1"}},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
OAuth2Options: &authclient.OAuth2Options{
Expand Down Expand Up @@ -134,7 +134,7 @@ func getOAuthServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
oAuth2TokenValue := "foo"
t.Run("ensure custom headers propagated correctly", func(t *testing.T) {
require.Equal(t, "v1", r.Header.Get("h1"))
require.Equal(t, "v1", r.Header.Get("H1"))
})
if r.URL.String() == handlerToken {
w.Header().Set("Content-Type", "application/json")
Expand Down
Loading