From 4daa652c00a56780453de25e814835bcfaff33f1 Mon Sep 17 00:00:00 2001 From: Jeremiah Harbach Date: Tue, 30 Jun 2026 16:58:38 -0500 Subject: [PATCH] Set request host authority from Host header Apply special handling for configured Host by setting req.Host, add regression test coverage, and document header behavior and SNI guidance in README. Signed-off-by: Jeremiah Harbach --- README.md | 20 ++++++++++++++++++++ cmd/main_test.go | 33 +++++++++++++++++++++++++++++++++ exporter/util.go | 4 ++++ 3 files changed, 57 insertions(+) diff --git a/README.md b/README.md index 75db454e..71839885 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,26 @@ This exporter allows you to use a field of the metric as the (unix/epoch) timest TLS configuration supported by this exporter can be found at [exporter-toolkit/web](https://github.com/prometheus/exporter-toolkit/blob/v0.9.0/docs/web-configuration.md) +## Custom request headers + +You can set per-module request headers with `modules..headers`. + +The `Host` header is handled specially. If a module config contains `Host`, the exporter sets the request host authority (`req.Host`) to that value. This is useful when probing an IP target while routing by hostname through a gateway or proxy. + +When probing HTTPS endpoints by IP and overriding host authority, also set TLS server name under `http_client_config.tls_config.server_name` so SNI and certificate validation use the same hostname. + +Example: + +```yaml +modules: + ip_with_host_routing: + headers: + Host: my-service.example.com + http_client_config: + tls_config: + server_name: my-service.example.com +``` + ## Sending body content for HTTP `POST` If `modules..body` paramater is set in config, it will be sent by the exporter as the body content in the scrape request. The HTTP method will also be set as 'POST' in this case. diff --git a/cmd/main_test.go b/cmd/main_test.go index 9499d386..ad0b5581 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -307,6 +307,39 @@ func TestHTTPHeaders(t *testing.T) { } } +func TestHostHeaderSetsRequestHost(t *testing.T) { + hostHeader := "integration-service.preventicedev.com" + + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Host; got != hostHeader { + t.Errorf("Unexpected request host: expected %q, got %q", hostHeader, got) + } + w.WriteHeader(http.StatusOK) + })) + defer target.Close() + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL, nil) + recorder := httptest.NewRecorder() + c := config.Config{ + Modules: map[string]config.Module{ + "default": { + Headers: map[string]string{ + "Host": hostHeader, + }, + }, + }, + } + + probeHandler(recorder, req, promslog.NewNopLogger(), c) + + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Setting host header failed unexpectedly. Got: %s", body) + } +} + // Test is the body template is correctly rendered func TestBodyPostTemplate(t *testing.T) { bodyTests := []struct { diff --git a/exporter/util.go b/exporter/util.go index ffdf5fd3..710c6cea 100644 --- a/exporter/util.go +++ b/exporter/util.go @@ -177,6 +177,10 @@ func (f *JSONFetcher) FetchJSON(endpoint string) ([]byte, error) { } for key, value := range f.module.Headers { + if strings.EqualFold(key, "Host") { + req.Host = value + continue + } req.Header.Add(key, value) } if req.Header.Get("Accept") == "" {