Repository: traefik/traefik (language: go)

Issue:
Add configuration for valid status codes of the health check
### Do you want to request a *feature* or report a *bug*?

Feature

### What did you expect to see?

At the moment traefik interprets all HTTP status codes >= 200 and < 400 as healthy state of a backend. But this doesn't fit for all services out there. So it would be nice to have the possibility to configure the status codes which are representing a healthy backend.



Discussion / hints:
Hello @coder-hugo,

Just to confirm, you are requesting a feature by which you can restrict the "success" to a smaller range? So that for example 301's may be a "fail"?

Or are you wanting a 400 to be a "success"?
For my concrete use case a 200 and 302 should be handled as healthy but a 303 as unhealthy. So I'm not sure if restricting the range is the correct solution. But I'm also thinking that it's no good idea that you have to provide all valid status codes in the configuration. I could think about passing valid ranges for the configuration, e.g: `200-299,302`
Having a similar issue with 303s with the Ceph Dashboard. If I can remove 303s from the pool then it will work as expected.
@mclamb in our case it was also the Ceph Dashboard. In some of the bugfix releases of the 14.2.x branch they added the functionality to disable the redirect and to return an error instead: https://docs.ceph.com/docs/master/mgr/dashboard/#disable-the-redirection
So if you are using ceph 14.2.x or higher you should give it a try.
Thanks @coder-hugo ! Indeed I did happen across that setting and made the change so it is now working as expected with the Traefik health check for the Ceph Dashboard LB pool working properly. That being said.. it would still be nice to configure the valid codes in Traefik too.
I'm also inquire by this feature request.
HTTP 400 could be not expected, however, since certain backends require an authentication, we may assume it is healthy as soon as it responds (ie: SOAP Web Services).

Fix patch:
diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml
index 3c91d6f7da..ad2419ca48 100644
--- a/docs/content/reference/dynamic-configuration/docker-labels.yml
+++ b/docs/content/reference/dynamic-configuration/docker-labels.yml
@@ -153,6 +153,7 @@
 - "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.status=42"
 - "traefik.http.services.service01.loadbalancer.healthcheck.port=42"
 - "traefik.http.services.service01.loadbalancer.healthcheck.scheme=foobar"
 - "traefik.http.services.service01.loadbalancer.healthcheck.mode=foobar"
diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml
index fc571b0268..fc2b54961a 100644
--- a/docs/content/reference/dynamic-configuration/file.toml
+++ b/docs/content/reference/dynamic-configuration/file.toml
@@ -56,6 +56,7 @@
           mode = "foobar"
           path = "foobar"
           method = "foobar"
+          status = 42
           port = 42
           interval = "42s"
           timeout = "42s"
diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml
index 808528ef98..d97d030f3c 100644
--- a/docs/content/reference/dynamic-configuration/file.yaml
+++ b/docs/content/reference/dynamic-configuration/file.yaml
@@ -61,6 +61,7 @@ http:
           mode: foobar
           path: foobar
           method: foobar
+          status: 42
           port: 42
           interval: 42s
           timeout: 42s
diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md
index 9fa147791c..9c0c3e41f2 100644
--- a/docs/content/reference/dynamic-configuration/kv-ref.md
+++ b/docs/content/reference/dynamic-configuration/kv-ref.md
@@ -220,6 +220,7 @@
 | `traefik/http/services/Service01/loadBalancer/healthCheck/path` | `foobar` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/port` | `42` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/scheme` | `foobar` |
+| `traefik/http/services/Service01/loadBalancer/healthCheck/status` | `42` |
 | `traefik/http/services/Service01/loadBalancer/healthCheck/timeout` | `42s` |
 | `traefik/http/services/Service01/loadBalancer/passHostHeader` | `true` |
 | `traefik/http/services/Service01/loadBalancer/responseForwarding/flushInterval` | `42s` |
diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json
index 5bc8e9e7ca..2bd780b0c2 100644
--- a/docs/content/reference/dynamic-configuration/marathon-labels.json
+++ b/docs/content/reference/dynamic-configuration/marathon-labels.json
@@ -153,6 +153,7 @@
 "traefik.http.services.service01.loadbalancer.healthcheck.interval": "42s",
 "traefik.http.services.service01.loadbalancer.healthcheck.path": "foobar",
 "traefik.http.services.service01.loadbalancer.healthcheck.method": "foobar",
+"traefik.http.services.service01.loadbalancer.healthcheck.status": "42",
 "traefik.http.services.service01.loadbalancer.healthcheck.port": "42",
 "traefik.http.services.service01.loadbalancer.healthcheck.scheme": "foobar",
 "traefik.http.services.service01.loadbalancer.healthcheck.mode": "foobar",
diff --git a/docs/content/routing/providers/consul-catalog.md b/docs/content/routing/providers/consul-catalog.md
index 16e9f531b5..6d2ddb8a27 100644
--- a/docs/content/routing/providers/consul-catalog.md
+++ b/docs/content/routing/providers/consul-catalog.md
@@ -193,6 +193,14 @@ you'd add the tag `traefik.http.services.{name-of-your-choice}.loadbalancer.pass
     traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.status`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```yaml
+    traefik.http.services.myservice.loadbalancer.healthcheck.status=42
+    ```
+
 ??? 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 0ebdf827f5..eacf78956a 100644
--- a/docs/content/routing/providers/docker.md
+++ b/docs/content/routing/providers/docker.md
@@ -347,6 +347,14 @@ you'd add the label `traefik.http.services.<name-of-your-choice>.loadbalancer.pa
     - "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar"
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.status`"
+
+    See [health check](../services/index.md#health-check) for more information.
+
+    ```yaml
+    - "traefik.http.services.myservice.loadbalancer.healthcheck.status=42"
+    ```
+
 ??? 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 0dad427814..9cd1806416 100644
--- a/docs/content/routing/providers/ecs.md
+++ b/docs/content/routing/providers/ecs.md
@@ -195,6 +195,14 @@ you'd add the label `traefik.http.services.{name-of-your-choice}.loadbalancer.pa
     traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.status`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```yaml
+    traefik.http.services.myservice.loadbalancer.healthcheck.status=42
+    ```
+
 ??? 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 8771c4cdb1..bce4d7703d 100644
--- a/docs/content/routing/providers/kv.md
+++ b/docs/content/routing/providers/kv.md
@@ -172,6 +172,14 @@ A Story of key & values
     |-------------------------------------------------------------------|----------|
     | `traefik/http/services/myservice/loadbalancer/healthcheck/method` | `foobar` |
 
+??? info "`traefik/http/services/<service_name>/loadbalancer/healthcheck/status`"
+
+    See [health check](../services/index.md#health-check) for more information.
+
+    | Key (Path)                                                        | Value |
+    |-------------------------------------------------------------------|-------|
+    | `traefik/http/services/myservice/loadbalancer/healthcheck/status` | `42`  |
+
 ??? 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 7c215f1b67..19a8a8ddea 100644
--- a/docs/content/routing/providers/marathon.md
+++ b/docs/content/routing/providers/marathon.md
@@ -222,6 +222,14 @@ For example, to change the passHostHeader behavior, you'd add the label `"traefi
     "traefik.http.services.myservice.loadbalancer.healthcheck.method": "foobar"
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.status`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```json
+    "traefik.http.services.myservice.loadbalancer.healthcheck.status": "42"
+    ```
+
 ??? 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/nomad.md b/docs/content/routing/providers/nomad.md
index ccc6316e77..e4d24a942b 100644
--- a/docs/content/routing/providers/nomad.md
+++ b/docs/content/routing/providers/nomad.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.status`"
+
+    See [health check](../services/index.md#health-check) for more information.
+
+    ```yaml
+    traefik.http.services.myservice.loadbalancer.healthcheck.status=42
+    ```
+
 ??? 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 fef132c4b4..7d392889ee 100644
--- a/docs/content/routing/providers/rancher.md
+++ b/docs/content/routing/providers/rancher.md
@@ -228,6 +228,14 @@ you'd add the label `traefik.http.services.{name-of-your-choice}.loadbalancer.pa
     - "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar"
     ```
 
+??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.status`"
+    
+    See [health check](../services/index.md#health-check) for more information.
+    
+    ```yaml
+    - "traefik.http.services.myservice.loadbalancer.healthcheck.status=42"
+    ```
+
 ??? 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 800471cf81..5e2766f0e5 100644
--- a/docs/content/routing/services/index.md
+++ b/docs/content/routing/services/index.md
@@ -316,7 +316,7 @@ On subsequent requests, to keep the session alive with the same server, the clie
 #### Health Check
 
 Configure health check to remove unhealthy servers from the load balancing rotation.
-Traefik will consider your HTTP(s) servers healthy as long as they return status codes between `2XX` and `3XX` to the health check requests (carried out every `interval`).
+Traefik will consider HTTP(s) servers healthy as long as they return a status code to the health check request (carried out every `interval`) between `2XX` and `3XX`, or matching the configured status.
 For gRPC servers, Traefik will consider them healthy as long as they return `SERVING` to [gRPC health check v1](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) requests.
 
 To propagate status changes (e.g. all servers of this service are down) upwards, HealthCheck must also be enabled on the parent(s) of this service.
@@ -333,6 +333,7 @@ Below are the available options for the health check mechanism:
 - `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.
+- `status` (optional), defines the expected HTTP status code of the response to the health check request.
 
 !!! info "Interval & Timeout Format"
 
diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go
index 89cbf7026a..746ced781d 100644
--- a/pkg/config/dynamic/http_config.go
+++ b/pkg/config/dynamic/http_config.go
@@ -237,6 +237,7 @@ type ServerHealthCheck struct {
 	Mode            string            `json:"mode,omitempty" toml:"mode,omitempty" yaml:"mode,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"`
+	Status          int               `json:"status,omitempty" toml:"status,omitempty" yaml:"status,omitempty" export:"true"`
 	Port            int               `json:"port,omitempty" toml:"port,omitempty,omitzero" yaml:"port,omitempty" export:"true"`
 	Interval        ptypes.Duration   `json:"interval,omitempty" toml:"interval,omitempty" yaml:"interval,omitempty" export:"true"`
 	Timeout         ptypes.Duration   `json:"timeout,omitempty" toml:"timeout,omitempty" yaml:"timeout,omitempty" export:"true"`
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 58cbcf6e48..7a590d3f17 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -173,10 +173,14 @@ func (shc *ServiceHealthChecker) checkHealthHTTP(ctx context.Context, target *ur
 
 	defer resp.Body.Close()
 
-	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
+	if shc.config.Status == 0 && (resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest) {
 		return fmt.Errorf("received error status code: %v", resp.StatusCode)
 	}
 
+	if shc.config.Status != 0 && shc.config.Status != resp.StatusCode {
+		return fmt.Errorf("received error status code: %v expected status code: %v", resp.StatusCode, shc.config.Status)
+	}
+
 	return nil
 }
 


Test patch:
diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go
index 702e9bffe4..19d9743879 100644
--- a/pkg/config/label/label_test.go
+++ b/pkg/config/label/label_test.go
@@ -151,6 +151,7 @@ func TestDecodeConfiguration(t *testing.T) {
 		"traefik.http.services.Service0.loadbalancer.healthcheck.interval":             "1s",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.path":                 "foobar",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.method":               "foobar",
+		"traefik.http.services.Service0.loadbalancer.healthcheck.status":               "401",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.port":                 "42",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.scheme":               "foobar",
 		"traefik.http.services.Service0.loadbalancer.healthcheck.mode":                 "foobar",
@@ -168,6 +169,7 @@ func TestDecodeConfiguration(t *testing.T) {
 		"traefik.http.services.Service1.loadbalancer.healthcheck.interval":             "1s",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.path":                 "foobar",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.method":               "foobar",
+		"traefik.http.services.Service1.loadbalancer.healthcheck.status":               "401",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.port":                 "42",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.scheme":               "foobar",
 		"traefik.http.services.Service1.loadbalancer.healthcheck.mode":                 "foobar",
@@ -655,6 +657,7 @@ func TestDecodeConfiguration(t *testing.T) {
 							Mode:     "foobar",
 							Path:     "foobar",
 							Method:   "foobar",
+							Status:   401,
 							Port:     42,
 							Interval: ptypes.Duration(time.Second),
 							Timeout:  ptypes.Duration(time.Second),
@@ -684,6 +687,7 @@ func TestDecodeConfiguration(t *testing.T) {
 							Mode:     "foobar",
 							Path:     "foobar",
 							Method:   "foobar",
+							Status:   401,
 							Port:     42,
 							Interval: ptypes.Duration(time.Second),
 							Timeout:  ptypes.Duration(time.Second),
@@ -1147,6 +1151,7 @@ func TestEncodeConfiguration(t *testing.T) {
 							Scheme:   "foobar",
 							Path:     "foobar",
 							Method:   "foobar",
+							Status:   401,
 							Port:     42,
 							Interval: ptypes.Duration(time.Second),
 							Timeout:  ptypes.Duration(time.Second),
@@ -1174,6 +1179,7 @@ func TestEncodeConfiguration(t *testing.T) {
 							Scheme:   "foobar",
 							Path:     "foobar",
 							Method:   "foobar",
+							Status:   401,
 							Port:     42,
 							Interval: ptypes.Duration(time.Second),
 							Timeout:  ptypes.Duration(time.Second),
@@ -1335,6 +1341,7 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Interval":             "1000000000",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Path":                 "foobar",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Method":               "foobar",
+		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Status":               "401",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Port":                 "42",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Scheme":               "foobar",
 		"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Timeout":              "1000000000",
@@ -1351,6 +1358,7 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Interval":             "1000000000",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Path":                 "foobar",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Method":               "foobar",
+		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Status":               "401",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Port":                 "42",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Scheme":               "foobar",
 		"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Timeout":              "1000000000",
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index b1e6d4f117..5bc08bb941 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -249,6 +249,7 @@ func TestServiceHealthChecker_Launch(t *testing.T) {
 	testCases := []struct {
 		desc                  string
 		mode                  string
+		status                int
 		server                StartTestServer
 		expNumRemovedServers  int
 		expNumUpsertedServers int
@@ -263,6 +264,15 @@ func TestServiceHealthChecker_Launch(t *testing.T) {
 			expGaugeValue:         1,
 			targetStatus:          runtime.StatusUp,
 		},
+		{
+			desc:                  "healthy server staying healthy, with custom code status check",
+			server:                newHTTPServer(http.StatusNotFound),
+			status:                http.StatusNotFound,
+			expNumRemovedServers:  0,
+			expNumUpsertedServers: 1,
+			expGaugeValue:         1,
+			targetStatus:          runtime.StatusUp,
+		},
 		{
 			desc:                  "healthy server staying healthy (StatusNoContent)",
 			server:                newHTTPServer(http.StatusNoContent),
@@ -287,6 +297,15 @@ func TestServiceHealthChecker_Launch(t *testing.T) {
 			expGaugeValue:         0,
 			targetStatus:          runtime.StatusDown,
 		},
+		{
+			desc:                  "healthy server becoming sick, with custom code status check",
+			server:                newHTTPServer(http.StatusOK),
+			status:                http.StatusServiceUnavailable,
+			expNumRemovedServers:  1,
+			expNumUpsertedServers: 0,
+			expGaugeValue:         0,
+			targetStatus:          runtime.StatusDown,
+		},
 		{
 			desc:                  "healthy server toggling to sick and back to healthy",
 			server:                newHTTPServer(http.StatusServiceUnavailable, http.StatusOK),
@@ -348,6 +367,7 @@ func TestServiceHealthChecker_Launch(t *testing.T) {
 
 			config := &dynamic.ServerHealthCheck{
 				Mode:     test.mode,
+				Status:   test.status,
 				Path:     "/path",
 				Interval: ptypes.Duration(500 * time.Millisecond),
 				Timeout:  ptypes.Duration(499 * time.Millisecond),