Skip to content

Commit

Permalink
feat(httpclient): support appending the same header multiple times
Browse files Browse the repository at this point in the history
See #829
  • Loading branch information
sathieu committed Mar 8, 2024
1 parent 9dc8124 commit 5a057a4
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 28 deletions.
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)
}
} 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
14 changes: 7 additions & 7 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,9 +55,9 @@ 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) {
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

0 comments on commit 5a057a4

Please sign in to comment.