Repository: traefik/traefik (language: go)

Issue:
Support fetching headers only for HTTP health check
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### Do you want to request a feature or report a bug?
Feature

###  What did you expect to see?

It would be great to see this feature in the health check:

Be able to select the headers only in an HTTP check

Use case:
Minio uses a healthcheck API but only gives back the header, anything different from a 200 is indicative that something is wrong with the node.
According to the minio [doc](https://docs.min.io/minio/baremetal/monitoring/healthcheck-probe.html)

```
curl -I http://minio.example.com:9000/minio/health/live
```

result in

```
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 0
Content-Security-Policy: block-all-mixed-content
Server: MinIO
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
X-Amz-Request-Id: 16F6A51C19B2FD3A
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block
Date: Wed, 08 Jun 2022 12:30:53 GMT
```
Traefik attempts to health check like so, it fails

```yml
services:
    minio-api:
      loadBalancer:
        healthCheck:
          path: /minio/health/live/
          port: 9000  
          interval: 10s
          timeout: 3s
        servers:
        - url  "http://172.26.3.41:9000"
        - url: "http://172.26.3.42:9000"
        - url: "http://172.26.3.43:9000"
        - url: "http://172.26.3.44:9000"
```


Discussion / hints:

Fix patch:
diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml
index 8ae452c895..6716ef8e52 100644
--- a/docs/content/reference/dynamic-configuration/docker-labels.yml
+++ b/docs/content/reference/dynamic-configuration/docker-labels.yml
@@ -151,10 +151,10 @@
 - "traefik.http.services.service01.loadbalancer.healthcheck.hostname=foobar"
 - "traefik.http.services.service01.loadbalancer.healthcheck.interval=foobar"
 - "traefik.http.services.service01.loadbalancer.healthcheck.path=foobar"
+- "traefik.http.services.service01.loadbalancer.healthcheck.method=foobar"
 - "traefik.http.services.service01.loadbalancer.healthcheck.port=42"
 - "traefik.http.services.service01.loadbalancer.healthcheck.scheme=foobar"
 - "traefik.http.services.service01.loadbalancer.healthcheck.timeout=foobar"
-- "traefik.http.services.service01.loadbalancer.healthcheck.followredirects=true"
 - "traefik.http.services.service01.loadbalancer.passhostheader=true"
 - "traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval=foobar"
 - "traefik.http.services.service01.loadbalancer.serverstransport=foobar"
diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml
index 0b640713eb..a7e23da179 100644
--- a/docs/content/reference/dynamic-configuration/file.toml
+++ b/docs/content/reference/dynamic-configuration/file.toml
@@ -54,6 +54,7 @@
         [http.services.Service01.loadBalancer.healthCheck]
           scheme = "foobar"
           path = "foobar"
+          method = "foobar"
           port = 42
           interval = "foobar"
           timeout = "foobar"
diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml
index ddbade43ef..7d95a4e8f2 100644
--- a/docs/content/reference/dynamic-configuration/file.yaml
+++ b/docs/content/reference/dynamic-configuration/file.yaml
@@ -59,6 +59,7 @@ http:
         healthCheck:
           scheme: foobar
           path: foobar
+          method: foobar
           port: 42
           interval: foobar
           timeout: foobar
diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md
index df3169d223..7927f2687e 100644
--- a/docs/content/reference/dynamic-configuration/kv-ref.md
+++ b/docs/content/reference/dynamic-configuration/kv-ref.md
@@ -207,6 +207,7 @@
 | `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name1` | `foobar` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/hostname` | `foobar` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/interval` | `foobar` |
+| `traefik/http/services/Service01/loadBalancer/healthCheck/method` | `foobar` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/path` | `foobar` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/port` | `42` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/scheme` | `foobar` |
diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json
index b78ca57b50..7eba469894 100644
--- a/docs/content/reference/dynamic-configuration/marathon-labels.json
+++ b/docs/content/reference/dynamic-configuration/marathon-labels.json
@@ -151,10 +151,10 @@
 "traefik.http.services.service01.loadbalancer.healthcheck.hostname": "foobar",
 "traefik.http.services.service01.loadbalancer.healthcheck.interval": "foobar",
 "traefik.http.services.service01.loadbalancer.healthcheck.path": "foobar",
+"traefik.http.services.service01.loadbalancer.healthcheck.method": "foobar",
 "traefik.http.services.service01.loadbalancer.healthcheck.port": "42",
 "traefik.http.services.service01.loadbalancer.healthcheck.scheme": "foobar",
 "traefik.http.services.service01.loadbalancer.healthcheck.timeout": "foobar",
-"traefik.http.services.service01.loadbalancer.healthcheck.followredirects": "true",
 "traefik.http.services.service01.loadbalancer.passhostheader": "true",
 "traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval": "foobar",
 "traefik.http.services.service01.loadbalancer.serverstransport": "foobar",
diff --git a/docs/content/routing/providers/consul-catalog.md b/docs/content/routing/providers/consul-catalog.md
index e1d5b1c65e..16e9f531b5 100644
--- a/docs/content/routing/providers/consul-catalog.md
+++ b/docs/content/routing/providers/consul-catalog.md
@@ -185,6 +185,14 @@ you'd add the tag `traefik.http.services.{name-of-your-choice}.loadbalancer.pass
     traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.method`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```yaml
+    traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar
+    ```
+
 ??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.port`"
     
     See [health check](../services/index.md#health-check) for more information.
diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md
index 5b53ba9245..942a2db250 100644
--- a/docs/content/routing/providers/docker.md
+++ b/docs/content/routing/providers/docker.md
@@ -339,6 +339,14 @@ you'd add the label `traefik.http.services.<name-of-your-choice>.loadbalancer.pa
     - "traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo"
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.method`"
+
+    See [health check](../services/index.md#health-check) for more information.
+
+    ```yaml
+    - "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar"
+    ```
+
 ??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.port`"
 
     See [health check](../services/index.md#health-check) for more information.
diff --git a/docs/content/routing/providers/ecs.md b/docs/content/routing/providers/ecs.md
index ac547fedd5..0dad427814 100644
--- a/docs/content/routing/providers/ecs.md
+++ b/docs/content/routing/providers/ecs.md
@@ -187,6 +187,14 @@ you'd add the label `traefik.http.services.{name-of-your-choice}.loadbalancer.pa
     traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.method`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```yaml
+    traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar
+    ```
+
 ??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.port`"
     
     See [health check](../services/index.md#health-check) for more information.
diff --git a/docs/content/routing/providers/kv.md b/docs/content/routing/providers/kv.md
index ccdd4a44e1..8771c4cdb1 100644
--- a/docs/content/routing/providers/kv.md
+++ b/docs/content/routing/providers/kv.md
@@ -164,6 +164,14 @@ A Story of key & values
     |-----------------------------------------------------------------|--------|
     | `traefik/http/services/myservice/loadbalancer/healthcheck/path` | `/foo` |
 
+??? info "`traefik/http/services/<service_name>/loadbalancer/healthcheck/method`"
+
+    See [health check](../services/index.md#health-check) for more information.
+
+    | Key (Path)                                                        | Value    |
+    |-------------------------------------------------------------------|----------|
+    | `traefik/http/services/myservice/loadbalancer/healthcheck/method` | `foobar` |
+
 ??? info "`traefik/http/services/<service_name>/loadbalancer/healthcheck/port`"
 
     See [health check](../services/index.md#health-check) for more information.
diff --git a/docs/content/routing/providers/marathon.md b/docs/content/routing/providers/marathon.md
index f201cd750c..7c215f1b67 100644
--- a/docs/content/routing/providers/marathon.md
+++ b/docs/content/routing/providers/marathon.md
@@ -214,6 +214,14 @@ For example, to change the passHostHeader behavior, you'd add the label `"traefi
     "traefik.http.services.myservice.loadbalancer.healthcheck.path": "/foo"
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.method`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```json
+    "traefik.http.services.myservice.loadbalancer.healthcheck.method": "foobar"
+    ```
+
 ??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.port`"
     
     See [health check](../services/index.md#health-check) for more information.
diff --git a/docs/content/routing/providers/rancher.md b/docs/content/routing/providers/rancher.md
index 6a07398076..fef132c4b4 100644
--- a/docs/content/routing/providers/rancher.md
+++ b/docs/content/routing/providers/rancher.md
@@ -220,6 +220,14 @@ you'd add the label `traefik.http.services.{name-of-your-choice}.loadbalancer.pa
     - "traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo"
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.method`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```yaml
+    - "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar"
+    ```
+
 ??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.port`"
     
     See [health check](../services/index.md#health-check) for more information.
diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md
index ca86b048c9..316d509996 100644
--- a/docs/content/routing/services/index.md
+++ b/docs/content/routing/services/index.md
@@ -330,6 +330,7 @@ Below are the available options for the health check mechanism:
 - `timeout` (default: 5s), defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy.
 - `headers` (optional), defines custom headers to be sent to the health check endpoint.
 - `followRedirects` (default: true), defines whether redirects should be followed during the health check calls.
+- `method` (default: GET), defines the HTTP method that will be used while connecting to the endpoint.
 
 !!! info "Interval & Timeout Format"
 
diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go
index 7cf4f64ff0..21e1ced7f0 100644
--- a/pkg/config/dynamic/http_config.go
+++ b/pkg/config/dynamic/http_config.go
@@ -214,6 +214,7 @@ func (s *Server) SetDefaults() {
 type ServerHealthCheck struct {
 	Scheme string `json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,omitempty" export:"true"`
 	Path   string `json:"path,omitempty" toml:"path,omitempty" yaml:"path,omitempty" export:"true"`
+	Method string `json:"method,omitempty" toml:"method,omitempty" yaml:"method,omitempty" export:"true"`
 	Port   int    `json:"port,omitempty" toml:"port,omitempty,omitzero" yaml:"port,omitempty" export:"true"`
 	// FIXME change string to ptypes.Duration
 	Interval string `json:"interval,omitempty" toml:"interval,omitempty" yaml:"interval,omitempty" export:"true"`
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 97a4264c9f..77a7743bb5 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
 	"sync"
 	"time"
 
@@ -60,6 +61,7 @@ type Options struct {
 	Hostname        string
 	Scheme          string
 	Path            string
+	Method          string
 	Port            int
 	FollowRedirects bool
 	Transport       http.RoundTripper
@@ -69,7 +71,7 @@ type Options struct {
 }
 
 func (opt Options) String() string {
-	return fmt.Sprintf("[Hostname: %s Headers: %v Path: %s Port: %d Interval: %s Timeout: %s FollowRedirects: %v]", opt.Hostname, opt.Headers, opt.Path, opt.Port, opt.Interval, opt.Timeout, opt.FollowRedirects)
+	return fmt.Sprintf("[Hostname: %s Headers: %v Path: %s Method: %s Port: %d Interval: %s Timeout: %s FollowRedirects: %v]", opt.Hostname, opt.Headers, opt.Path, opt.Method, opt.Port, opt.Interval, opt.Timeout, opt.FollowRedirects)
 }
 
 type backendURL struct {
@@ -101,8 +103,8 @@ func (b *BackendConfig) newRequest(serverURL *url.URL) (*http.Request, error) {
 	return http.NewRequest(http.MethodGet, u.String(), http.NoBody)
 }
 
-// this function adds additional http headers and hostname to http.request.
-func (b *BackendConfig) addHeadersAndHost(req *http.Request) *http.Request {
+// setRequestOptions sets all request options present on the BackendConfig.
+func (b *BackendConfig) setRequestOptions(req *http.Request) *http.Request {
 	if b.Options.Hostname != "" {
 		req.Host = b.Options.Hostname
 	}
@@ -110,6 +112,11 @@ func (b *BackendConfig) addHeadersAndHost(req *http.Request) *http.Request {
 	for k, v := range b.Options.Headers {
 		req.Header.Set(k, v)
 	}
+
+	if b.Options.Method != "" {
+		req.Method = strings.ToUpper(b.Options.Method)
+	}
+
 	return req
 }
 
@@ -246,7 +253,7 @@ func checkHealth(serverURL *url.URL, backend *BackendConfig) error {
 		return fmt.Errorf("failed to create HTTP request: %w", err)
 	}
 
-	req = backend.addHeadersAndHost(req)
+	req = backend.setRequestOptions(req)
 
 	client := http.Client{
 		Timeout:   backend.Options.Timeout,
diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go
index 375e9f64ef..3ff89eaa01 100644
--- a/pkg/server/service/service.go
+++ b/pkg/server/service/service.go
@@ -360,6 +360,7 @@ func buildHealthCheckOptions(ctx context.Context, lb healthcheck.Balancer, backe
 	return &healthcheck.Options{
 		Scheme:          hc.Scheme,
 		Path:            hc.Path,
+		Method:          hc.Method,
 		Port:            hc.Port,
 		Interval:        interval,
 		Timeout:         timeout,


Test patch:
diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go
index 4cda9c1777..04bce464ee 100644
--- a/pkg/config/label/label_test.go
+++ b/pkg/config/label/label_test.go
@@ -150,6 +150,7 @@ func TestDecodeConfiguration(t *testing.T) {
 		"traefik.http.services.Service0.loadbalancer.healthcheck.hostname":             "foobar",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.interval":             "foobar",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.path":                 "foobar",
+		"traefik.http.services.Service0.loadbalancer.healthcheck.method":               "foobar",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.port":                 "42",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.scheme":               "foobar",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.timeout":              "foobar",
@@ -165,6 +166,7 @@ func TestDecodeConfiguration(t *testing.T) {
 		"traefik.http.services.Service1.loadbalancer.healthcheck.hostname":             "foobar",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.interval":             "foobar",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.path":                 "foobar",
+		"traefik.http.services.Service1.loadbalancer.healthcheck.method":               "foobar",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.port":                 "42",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.scheme":               "foobar",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.timeout":              "foobar",
@@ -649,6 +651,7 @@ func TestDecodeConfiguration(t *testing.T) {
 						HealthCheck: &dynamic.ServerHealthCheck{
 							Scheme:   "foobar",
 							Path:     "foobar",
+							Method:   "foobar",
 							Port:     42,
 							Interval: "foobar",
 							Timeout:  "foobar",
@@ -676,6 +679,7 @@ func TestDecodeConfiguration(t *testing.T) {
 						HealthCheck: &dynamic.ServerHealthCheck{
 							Scheme:   "foobar",
 							Path:     "foobar",
+							Method:   "foobar",
 							Port:     42,
 							Interval: "foobar",
 							Timeout:  "foobar",
@@ -1138,6 +1142,7 @@ func TestEncodeConfiguration(t *testing.T) {
 						HealthCheck: &dynamic.ServerHealthCheck{
 							Scheme:   "foobar",
 							Path:     "foobar",
+							Method:   "foobar",
 							Port:     42,
 							Interval: "foobar",
 							Timeout:  "foobar",
@@ -1164,6 +1169,7 @@ func TestEncodeConfiguration(t *testing.T) {
 						HealthCheck: &dynamic.ServerHealthCheck{
 							Scheme:   "foobar",
 							Path:     "foobar",
+							Method:   "foobar",
 							Port:     42,
 							Interval: "foobar",
 							Timeout:  "foobar",
@@ -1324,6 +1330,7 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Hostname":             "foobar",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Interval":             "foobar",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Path":                 "foobar",
+		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Method":               "foobar",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Port":                 "42",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Scheme":               "foobar",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Timeout":              "foobar",
@@ -1339,6 +1346,7 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Hostname":             "foobar",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Interval":             "foobar",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Path":                 "foobar",
+		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Method":               "foobar",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Port":                 "42",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Scheme":               "foobar",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Timeout":              "foobar",
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index fc71eafddf..914f48291f 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -288,13 +288,14 @@ func TestNewRequest(t *testing.T) {
 	}
 }
 
-func TestAddHeadersAndHost(t *testing.T) {
+func TestRequestOptions(t *testing.T) {
 	testCases := []struct {
 		desc             string
 		serverURL        string
 		options          Options
 		expectedHostname string
 		expectedHeader   string
+		expectedMethod   string
 	}{
 		{
 			desc:      "override hostname",
@@ -305,6 +306,7 @@ func TestAddHeadersAndHost(t *testing.T) {
 			},
 			expectedHostname: "myhost",
 			expectedHeader:   "",
+			expectedMethod:   http.MethodGet,
 		},
 		{
 			desc:      "not override hostname",
@@ -315,6 +317,7 @@ func TestAddHeadersAndHost(t *testing.T) {
 			},
 			expectedHostname: "backend1:80",
 			expectedHeader:   "",
+			expectedMethod:   http.MethodGet,
 		},
 		{
 			desc:      "custom header",
@@ -326,6 +329,7 @@ func TestAddHeadersAndHost(t *testing.T) {
 			},
 			expectedHostname: "backend1:80",
 			expectedHeader:   "foo",
+			expectedMethod:   http.MethodGet,
 		},
 		{
 			desc:      "custom header with hostname override",
@@ -337,6 +341,17 @@ func TestAddHeadersAndHost(t *testing.T) {
 			},
 			expectedHostname: "myhost",
 			expectedHeader:   "foo",
+			expectedMethod:   http.MethodGet,
+		},
+		{
+			desc:      "custom method",
+			serverURL: "http://backend1:80",
+			options: Options{
+				Path:   "/",
+				Method: http.MethodHead,
+			},
+			expectedHostname: "backend1:80",
+			expectedMethod:   http.MethodHead,
 		},
 	}
 
@@ -353,11 +368,12 @@ func TestAddHeadersAndHost(t *testing.T) {
 			req, err := backend.newRequest(u)
 			require.NoError(t, err, "failed to create new backend request")
 
-			req = backend.addHeadersAndHost(req)
+			req = backend.setRequestOptions(req)
 
 			assert.Equal(t, "http://backend1:80/", req.URL.String())
 			assert.Equal(t, test.expectedHostname, req.Host)
 			assert.Equal(t, test.expectedHeader, req.Header.Get("Custom-Header"))
+			assert.Equal(t, test.expectedMethod, req.Method)
 		})
 	}
 }