Repository: prometheus/common
Pull request #651 (2024-06-07 14:24:50)

Fix HTTPClientConfig JSON marshalling
I think https://github.com/prometheus/common/pull/416 introduced an issue. `json` marshalling doesn't support `inline` and so when `HTTPClientConfig` gets marshalled to JSON it gets marshalled to something like this:

```
{"http_headers":{"Headers":{"Header-Name":{}}}
```

while I would expect something like this:

```
{"http_headers":{"Header-Name":{}}
```

This PR should fix it.


Patch:
diff --git a/config/headers.go b/config/headers.go
index 7c3149e..4a0be4a 100644
--- a/config/headers.go
+++ b/config/headers.go
@@ -17,6 +17,7 @@
 package config
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"os"
@@ -50,17 +51,22 @@ var reservedHeaders = map[string]struct{}{
 
 // Headers represents the configuration for HTTP headers.
 type Headers struct {
-	Headers map[string]Header `yaml:",inline" json:",inline"`
+	Headers map[string]Header `yaml:",inline"`
 	dir     string
 }
 
-// Headers represents the configuration for HTTP headers.
+// Header represents the configuration for a single HTTP header.
 type Header struct {
 	Values  []string `yaml:"values,omitempty" json:"values,omitempty"`
 	Secrets []Secret `yaml:"secrets,omitempty" json:"secrets,omitempty"`
 	Files   []string `yaml:"files,omitempty" json:"files,omitempty"`
 }
 
+func (h Headers) MarshalJSON() ([]byte, error) {
+	// Inline the Headers map when serializing JSON because json encoder doesn't support "inline" directive.
+	return json.Marshal(h.Headers)
+}
+
 // SetDirectory records the directory to make headers file relative to the
 // configuration file.
 func (h *Headers) SetDirectory(dir string) {


Test patch:
diff --git a/config/http_config_test.go b/config/http_config_test.go
index 67b7408..b66a469 100644
--- a/config/http_config_test.go
+++ b/config/http_config_test.go
@@ -35,6 +35,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
 	"gopkg.in/yaml.v2"
 )
 
@@ -2004,6 +2005,91 @@ func TestMarshalURLWithSecret(t *testing.T) {
 	}
 }
 
+func TestHTTPClientConfig_Marshal(t *testing.T) {
+	proxyURL, err := url.Parse("http://localhost:8080")
+	require.NoError(t, err)
+
+	t.Run("without HTTP headers", func(t *testing.T) {
+		config := &HTTPClientConfig{
+			ProxyConfig: ProxyConfig{
+				ProxyURL: URL{proxyURL},
+			},
+		}
+
+		t.Run("YAML", func(t *testing.T) {
+			actualYAML, err := yaml.Marshal(config)
+			require.NoError(t, err)
+			require.YAMLEq(t, `
+proxy_url: "http://localhost:8080"
+follow_redirects: false
+enable_http2: false
+http_headers: null
+`, string(actualYAML))
+
+			// Unmarshalling the YAML should get the same struct in input.
+			actual := &HTTPClientConfig{}
+			require.NoError(t, yaml.Unmarshal(actualYAML, actual))
+			require.Equal(t, config, actual)
+		})
+
+		t.Run("JSON", func(t *testing.T) {
+			actualJSON, err := json.Marshal(config)
+
+			require.NoError(t, err)
+			require.JSONEq(t, `{
+				"proxy_url":"http://localhost:8080",
+				"tls_config":{"insecure_skip_verify":false},
+				"follow_redirects":false,
+				"enable_http2":false,
+				"http_headers":null
+			}`, string(actualJSON))
+
+			// Unmarshalling the JSON should get the same struct in input.
+			actual := &HTTPClientConfig{}
+			require.NoError(t, json.Unmarshal(actualJSON, actual))
+			require.Equal(t, config, actual)
+		})
+	})
+
+	t.Run("with HTTP headers", func(t *testing.T) {
+		config := &HTTPClientConfig{
+			ProxyConfig: ProxyConfig{
+				ProxyURL: URL{proxyURL},
+			},
+			HTTPHeaders: &Headers{
+				Headers: map[string]Header{
+					"X-Test": {
+						Values: []string{"Value-1", "Value-2"},
+					},
+				},
+			},
+		}
+
+		actualYAML, err := yaml.Marshal(config)
+		require.NoError(t, err)
+		require.YAMLEq(t, `
+proxy_url: "http://localhost:8080"
+follow_redirects: false
+enable_http2: false
+http_headers:
+  X-Test:
+    values:
+    - Value-1
+    - Value-2
+`, string(actualYAML))
+
+		actualJSON, err := json.Marshal(config)
+		require.NoError(t, err)
+		require.JSONEq(t, `{
+			"proxy_url":"http://localhost:8080",
+			"tls_config":{"insecure_skip_verify":false},
+			"follow_redirects":false,
+			"enable_http2":false,
+			"http_headers":{"X-Test":{"values":["Value-1","Value-2"]}}
+		}`, string(actualJSON))
+	})
+}
+
 func TestOAuth2Proxy(t *testing.T) {
 	_, _, err := LoadHTTPConfigFile("testdata/http.conf.oauth2-proxy.good.yml")
 	if err != nil {