Repository: containous/traefik
Pull request #5806 (2019-11-09 16:56:12)

fix: stripPrefix middleware with empty resulting path.

### What does this PR do?

Add an option to allow to bypass the buggy behavior of the `stripPrefix` middleware.

### Motivation

 when the resulting striped path is empty, the path must be empty and not  `/`

### More

- [x] Added/updated tests
- [x] Added/updated documentation


dtomcej: Should each middleware have its own `func ensureLeadingSlash(str string) string`?

Is that something that could be provided by a base middleware utility/tools package? just so that we don't end up with one middleware behaving differently?
mpl: > Should each middleware have its own `func ensureLeadingSlash(str string) string`?
> 
> Is that something that could be provided by a base middleware utility/tools package? just so that we don't end up with one middleware behaving differently?

If we do that, we should have a func (name) that does not mislead about what it actually does :-)
ldez: for now I will not factorize.

Patch:
diff --git a/docs/content/middlewares/addprefix.md b/docs/content/middlewares/addprefix.md
index c41cd1d40..61afb60b0 100644
--- a/docs/content/middlewares/addprefix.md
+++ b/docs/content/middlewares/addprefix.md
@@ -58,4 +58,5 @@ http:
 
 ### `prefix`
 
-`prefix` is the string to add before the current path in the requested URL. It should include the leading slash (`/`).
+`prefix` is the string to add before the current path in the requested URL.
+It should include the leading slash (`/`).
diff --git a/docs/content/middlewares/stripprefix.md b/docs/content/middlewares/stripprefix.md
index f34ab014c..8a7f7f194 100644
--- a/docs/content/middlewares/stripprefix.md
+++ b/docs/content/middlewares/stripprefix.md
@@ -85,3 +85,85 @@ If your backend is serving assets (e.g., images or Javascript files), chances ar
 Continuing on the example, the backend should return `/products/shoes/image.png` (and not `/images.png` which Traefik would likely not be able to associate with the same backend).  
 
 The `X-Forwarded-Prefix` header can be queried to build such URLs dynamically.
+
+### `forceSlash`
+
+_Optional, Default=true_
+
+```yaml tab="Docker"
+labels:
+  - "traefik.http.middlewares.example.stripprefix.prefixes=/foobar"
+  - "traefik.http.middlewares.example.stripprefix.forceslash=false"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+  name: example
+spec:
+  stripPrefix:
+    prefixes:
+      - "/foobar"
+    forceSlash: false
+```
+
+```json tab="Marathon"
+"labels": {
+  "traefik.http.middlewares.example.stripprefix.prefixes": "/foobar",
+  "traefik.http.middlewares.example.stripprefix.forceslash": "false"
+}
+```
+
+```yaml tab="Rancher"
+labels:
+  - "traefik.http.middlewares.example.stripprefix.prefixes=/foobar"
+  - "traefik.http.middlewares.example.stripprefix.forceSlash=false"
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.example.stripPrefix]
+    prefixes = ["/foobar"]
+    forceSlash = false
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    example:
+      stripPrefix:
+        prefixes:
+          - "/foobar"
+        forceSlash: false
+```
+
+The `forceSlash` option makes sure that the resulting stripped path is not the empty string, by replacing it with `/` when necessary.
+
+This option was added to keep the initial (non-intuitive) behavior of this middleware, in order to avoid introducing a breaking change.
+
+It's recommended to explicitly set `forceSlash` to `false`.
+
+??? info "Behavior examples"
+    
+    - `forceSlash=true`
+    
+    | Path       | Prefix to strip | Result |
+    |------------|-----------------|--------|
+    | `/`        | `/`             | `/`    |
+    | `/foo`     | `/foo`          | `/`    |
+    | `/foo/`    | `/foo`          | `/`    |
+    | `/foo/`    | `/foo/`         | `/`    |
+    | `/bar`     | `/foo`          | `/bar` |
+    | `/foo/bar` | `/foo`          | `/bar` |
+    
+    - `forceSlash=false`
+    
+    | Path       | Prefix to strip | Result |
+    |------------|-----------------|--------|
+    | `/`        | `/`             | empty  |
+    | `/foo`     | `/foo`          | empty  |
+    | `/foo/`    | `/foo`          | `/`    |
+    | `/foo/`    | `/foo/`         | empty  |
+    | `/bar`     | `/foo`          | `/bar` |
+    | `/foo/bar` | `/foo`          | `/bar` |
diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml
index 722a4fdf4..19ce7cef5 100644
--- a/docs/content/reference/dynamic-configuration/docker-labels.yml
+++ b/docs/content/reference/dynamic-configuration/docker-labels.yml
@@ -103,6 +103,7 @@
 - "traefik.http.middlewares.middleware17.replacepathregex.regex=foobar"
 - "traefik.http.middlewares.middleware17.replacepathregex.replacement=foobar"
 - "traefik.http.middlewares.middleware18.retry.attempts=42"
+- "traefik.http.middlewares.middleware19.stripprefix.forceslash=true"
 - "traefik.http.middlewares.middleware19.stripprefix.prefixes=foobar, foobar"
 - "traefik.http.middlewares.middleware20.stripprefixregex.regex=foobar, foobar"
 - "traefik.http.routers.router0.entrypoints=foobar, foobar"
@@ -129,38 +130,22 @@
 - "traefik.http.routers.router1.tls.domains[1].main=foobar"
 - "traefik.http.routers.router1.tls.domains[1].sans=foobar, foobar"
 - "traefik.http.routers.router1.tls.options=foobar"
-- "traefik.http.services.service0.loadbalancer.healthcheck.headers.name0=foobar"
-- "traefik.http.services.service0.loadbalancer.healthcheck.headers.name1=foobar"
-- "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.port=42"
-- "traefik.http.services.service0.loadbalancer.healthcheck.scheme=foobar"
-- "traefik.http.services.service0.loadbalancer.healthcheck.timeout=foobar"
-- "traefik.http.services.service0.loadbalancer.passhostheader=true"
-- "traefik.http.services.service0.loadbalancer.responseforwarding.flushinterval=foobar"
-- "traefik.http.services.service0.loadbalancer.sticky=true"
-- "traefik.http.services.service0.loadbalancer.sticky.cookie.httponly=true"
-- "traefik.http.services.service0.loadbalancer.sticky.cookie.name=foobar"
-- "traefik.http.services.service0.loadbalancer.sticky.cookie.secure=true"
-- "traefik.http.services.service0.loadbalancer.server.port=foobar"
-- "traefik.http.services.service0.loadbalancer.server.scheme=foobar"
-- "traefik.http.services.service1.loadbalancer.healthcheck.headers.name0=foobar"
-- "traefik.http.services.service1.loadbalancer.healthcheck.headers.name1=foobar"
-- "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.port=42"
-- "traefik.http.services.service1.loadbalancer.healthcheck.scheme=foobar"
-- "traefik.http.services.service1.loadbalancer.healthcheck.timeout=foobar"
-- "traefik.http.services.service1.loadbalancer.passhostheader=true"
-- "traefik.http.services.service1.loadbalancer.responseforwarding.flushinterval=foobar"
-- "traefik.http.services.service1.loadbalancer.sticky=true"
-- "traefik.http.services.service1.loadbalancer.sticky.cookie.httponly=true"
-- "traefik.http.services.service1.loadbalancer.sticky.cookie.name=foobar"
-- "traefik.http.services.service1.loadbalancer.sticky.cookie.secure=true"
-- "traefik.http.services.service1.loadbalancer.server.port=foobar"
-- "traefik.http.services.service1.loadbalancer.server.scheme=foobar"
+- "traefik.http.services.service01.loadbalancer.healthcheck.headers.name0=foobar"
+- "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.path=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.passhostheader=true"
+- "traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval=foobar"
+- "traefik.http.services.service01.loadbalancer.sticky=true"
+- "traefik.http.services.service01.loadbalancer.sticky.cookie.httponly=true"
+- "traefik.http.services.service01.loadbalancer.sticky.cookie.name=foobar"
+- "traefik.http.services.service01.loadbalancer.sticky.cookie.secure=true"
+- "traefik.http.services.service01.loadbalancer.server.port=foobar"
+- "traefik.http.services.service01.loadbalancer.server.scheme=foobar"
 - "traefik.tcp.routers.tcprouter0.entrypoints=foobar, foobar"
 - "traefik.tcp.routers.tcprouter0.rule=foobar"
 - "traefik.tcp.routers.tcprouter0.service=foobar"
@@ -183,7 +168,5 @@
 - "traefik.tcp.routers.tcprouter1.tls.domains[1].sans=foobar, foobar"
 - "traefik.tcp.routers.tcprouter1.tls.options=foobar"
 - "traefik.tcp.routers.tcprouter1.tls.passthrough=true"
-- "traefik.tcp.services.tcpservice0.loadbalancer.server.port=foobar"
-- "traefik.tcp.services.tcpservice0.loadbalancer.terminationdelay=100"
-- "traefik.tcp.services.tcpservice1.loadbalancer.server.port=foobar"
-- "traefik.tcp.services.tcpservice1.loadbalancer.terminationdelay=100"
+- "traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay=42"
+- "traefik.tcp.services.tcpservice01.loadbalancer.server.port=foobar"
diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml
index 8027a082b..326212b6b 100644
--- a/docs/content/reference/dynamic-configuration/file.toml
+++ b/docs/content/reference/dynamic-configuration/file.toml
@@ -245,6 +245,7 @@
     [http.middlewares.Middleware19]
       [http.middlewares.Middleware19.stripPrefix]
         prefixes = ["foobar", "foobar"]
+        forceSlash = true
     [http.middlewares.Middleware20]
       [http.middlewares.Middleware20.stripPrefixRegex]
         regex = ["foobar", "foobar"]
@@ -284,25 +285,25 @@
           main = "foobar"
           sans = ["foobar", "foobar"]
   [tcp.services]
-    [tcp.services.TCPService0]
-      [tcp.services.TCPService0.loadBalancer]
-        terminationDelay = 100
+    [tcp.services.TCPService01]
+      [tcp.services.TCPService01.loadBalancer]
+        terminationDelay = 42
 
-        [[tcp.services.TCPService0.loadBalancer.servers]]
+        [[tcp.services.TCPService01.loadBalancer.servers]]
           address = "foobar"
 
-        [[tcp.services.TCPService0.loadBalancer.servers]]
+        [[tcp.services.TCPService01.loadBalancer.servers]]
           address = "foobar"
+    [tcp.services.TCPService02]
+      [tcp.services.TCPService02.weighted]
 
-    [tcp.services.TCPService1]
-      [tcp.services.TCPService1.loadBalancer]
-        terminationDelay = 100
-
-        [[tcp.services.TCPService1.loadBalancer.servers]]
-          address = "foobar"
+        [[tcp.services.TCPService02.weighted.services]]
+          name = "foobar"
+          weight = 42
 
-        [[tcp.services.TCPService1.loadBalancer.servers]]
-          address = "foobar"
+        [[tcp.services.TCPService02.weighted.services]]
+          name = "foobar"
+          weight = 42
 
 [tls]
 
diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml
index e8ab878b7..fe5d8c252 100644
--- a/docs/content/reference/dynamic-configuration/file.yaml
+++ b/docs/content/reference/dynamic-configuration/file.yaml
@@ -2,11 +2,11 @@ http:
   routers:
     Router0:
       entryPoints:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       middlewares:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       service: foobar
       rule: foobar
       priority: 42
@@ -14,21 +14,21 @@ http:
         options: foobar
         certResolver: foobar
         domains:
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
     Router1:
       entryPoints:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       middlewares:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       service: foobar
       rule: foobar
       priority: 42
@@ -36,14 +36,14 @@ http:
         options: foobar
         certResolver: foobar
         domains:
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
   services:
     Service01:
       loadBalancer:
@@ -53,8 +53,8 @@ http:
             secure: true
             httpOnly: true
         servers:
-          - url: foobar
-          - url: foobar
+        - url: foobar
+        - url: foobar
         healthCheck:
           scheme: foobar
           path: foobar
@@ -72,17 +72,17 @@ http:
       mirroring:
         service: foobar
         mirrors:
-          - name: foobar
-            percent: 42
-          - name: foobar
-            percent: 42
+        - name: foobar
+          percent: 42
+        - name: foobar
+          percent: 42
     Service03:
       weighted:
         services:
-          - name: foobar
-            weight: 42
-          - name: foobar
-            weight: 42
+        - name: foobar
+          weight: 42
+        - name: foobar
+          weight: 42
         sticky:
           cookie:
             name: foobar
@@ -95,8 +95,8 @@ http:
     Middleware01:
       basicAuth:
         users:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         usersFile: foobar
         realm: foobar
         removeHeader: true
@@ -111,8 +111,8 @@ http:
     Middleware03:
       chain:
         middlewares:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
     Middleware04:
       circuitBreaker:
         expression: foobar
@@ -121,8 +121,8 @@ http:
     Middleware06:
       digestAuth:
         users:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         usersFile: foobar
         removeHeader: true
         realm: foobar
@@ -130,8 +130,8 @@ http:
     Middleware07:
       errors:
         status:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         service: foobar
         query: foobar
     Middleware08:
@@ -145,8 +145,8 @@ http:
           insecureSkipVerify: true
         trustForwardHeader: true
         authResponseHeaders:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
     Middleware09:
       headers:
         customRequestHeaders:
@@ -157,23 +157,23 @@ http:
           name1: foobar
         accessControlAllowCredentials: true
         accessControlAllowHeaders:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         accessControlAllowMethods:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         accessControlAllowOrigin: foobar
         accessControlExposeHeaders:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         accessControlMaxAge: 42
         addVaryHeader: true
         allowedHosts:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         hostsProxyHeaders:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         sslRedirect: true
         sslTemporaryRedirect: true
         sslHost: foobar
@@ -198,13 +198,13 @@ http:
     Middleware10:
       ipWhiteList:
         sourceRange:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         ipStrategy:
           depth: 42
           excludedIPs:
-            - foobar
-            - foobar
+          - foobar
+          - foobar
     Middleware11:
       inFlightReq:
         amount: 42
@@ -212,8 +212,8 @@ http:
           ipstrategy:
             depth: 42
             excludedIPs:
-              - foobar
-              - foobar
+            - foobar
+            - foobar
           requestHeaderName: foobar
           requestHost: true
     Middleware12:
@@ -247,8 +247,8 @@ http:
           ipstrategy:
             depth: 42
             excludedIPs:
-              - foobar
-              - foobar
+            - foobar
+            - foobar
           requestHeaderName: foobar
           requestHost: true
     Middleware14:
@@ -274,19 +274,20 @@ http:
     Middleware19:
       stripPrefix:
         prefixes:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
+        forceSlash: true
     Middleware20:
       stripPrefixRegex:
         regex:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
 tcp:
   routers:
     TCPRouter0:
       entryPoints:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       service: foobar
       rule: foobar
       tls:
@@ -294,18 +295,18 @@ tcp:
         options: foobar
         certResolver: foobar
         domains:
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
     TCPRouter1:
       entryPoints:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       service: foobar
       rule: foobar
       tls:
@@ -313,60 +314,61 @@ tcp:
         options: foobar
         certResolver: foobar
         domains:
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
-          - main: foobar
-            sans:
-              - foobar
-              - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
+        - main: foobar
+          sans:
+          - foobar
+          - foobar
   services:
-    TCPService0:
+    TCPService01:
       loadBalancer:
-        terminationDelay: 100
+        terminationDelay: 42
         servers:
-          - address: foobar
-          - address: foobar
-    TCPService1:
-      loadBalancer:
-        terminationDelay: 100
-        servers:
-          - address: foobar
-          - address: foobar
+        - address: foobar
+        - address: foobar
+    TCPService02:
+      weighted:
+        services:
+        - name: foobar
+          weight: 42
+        - name: foobar
+          weight: 42
 tls:
   certificates:
-    - certFile: foobar
-      keyFile: foobar
-      stores:
-        - foobar
-        - foobar
-    - certFile: foobar
-      keyFile: foobar
-      stores:
-        - foobar
-        - foobar
+  - certFile: foobar
+    keyFile: foobar
+    stores:
+    - foobar
+    - foobar
+  - certFile: foobar
+    keyFile: foobar
+    stores:
+    - foobar
+    - foobar
   options:
     Options0:
       minVersion: foobar
       cipherSuites:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       clientAuth:
         caFiles:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         clientAuthType: foobar
       sniStrict: true
     Options1:
       minVersion: foobar
       cipherSuites:
-        - foobar
-        - foobar
+      - foobar
+      - foobar
       clientAuth:
         caFiles:
-          - foobar
-          - foobar
+        - foobar
+        - foobar
         clientAuthType: foobar
       sniStrict: true
   stores:
diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json
index 695e8c6bd..4294c82e0 100644
--- a/docs/content/reference/dynamic-configuration/marathon-labels.json
+++ b/docs/content/reference/dynamic-configuration/marathon-labels.json
@@ -103,6 +103,7 @@
 "traefik.http.middlewares.middleware17.replacepathregex.regex": "foobar",
 "traefik.http.middlewares.middleware17.replacepathregex.replacement": "foobar",
 "traefik.http.middlewares.middleware18.retry.attempts": "42",
+"traefik.http.middlewares.middleware19.stripprefix.forceslash": "true",
 "traefik.http.middlewares.middleware19.stripprefix.prefixes": "foobar, foobar",
 "traefik.http.middlewares.middleware20.stripprefixregex.regex": "foobar, foobar",
 "traefik.http.routers.router0.entrypoints": "foobar, foobar",
@@ -110,7 +111,6 @@
 "traefik.http.routers.router0.priority": "42",
 "traefik.http.routers.router0.rule": "foobar",
 "traefik.http.routers.router0.service": "foobar",
-"traefik.http.routers.router0.tls": "true",
 "traefik.http.routers.router0.tls.certresolver": "foobar",
 "traefik.http.routers.router0.tls.domains[0].main": "foobar",
 "traefik.http.routers.router0.tls.domains[0].sans": "foobar, foobar",
@@ -122,49 +122,30 @@
 "traefik.http.routers.router1.priority": "42",
 "traefik.http.routers.router1.rule": "foobar",
 "traefik.http.routers.router1.service": "foobar",
-"traefik.http.routers.router1.tls": "true",
 "traefik.http.routers.router1.tls.certresolver": "foobar",
 "traefik.http.routers.router1.tls.domains[0].main": "foobar",
 "traefik.http.routers.router1.tls.domains[0].sans": "foobar, foobar",
 "traefik.http.routers.router1.tls.domains[1].main": "foobar",
 "traefik.http.routers.router1.tls.domains[1].sans": "foobar, foobar",
 "traefik.http.routers.router1.tls.options": "foobar",
-"traefik.http.services.service0.loadbalancer.healthcheck.headers.name0": "foobar",
-"traefik.http.services.service0.loadbalancer.healthcheck.headers.name1": "foobar",
-"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.port": "42",
-"traefik.http.services.service0.loadbalancer.healthcheck.scheme": "foobar",
-"traefik.http.services.service0.loadbalancer.healthcheck.timeout": "foobar",
-"traefik.http.services.service0.loadbalancer.passhostheader": "true",
-"traefik.http.services.service0.loadbalancer.responseforwarding.flushinterval": "foobar",
-"traefik.http.services.service0.loadbalancer.sticky": "true",
-"traefik.http.services.service0.loadbalancer.sticky.cookie.httponly": "true",
-"traefik.http.services.service0.loadbalancer.sticky.cookie.name": "foobar",
-"traefik.http.services.service0.loadbalancer.sticky.cookie.secure": "true",
-"traefik.http.services.service0.loadbalancer.server.port": "foobar",
-"traefik.http.services.service0.loadbalancer.server.scheme": "foobar",
-"traefik.http.services.service1.loadbalancer.healthcheck.headers.name0": "foobar",
-"traefik.http.services.service1.loadbalancer.healthcheck.headers.name1": "foobar",
-"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.port": "42",
-"traefik.http.services.service1.loadbalancer.healthcheck.scheme": "foobar",
-"traefik.http.services.service1.loadbalancer.healthcheck.timeout": "foobar",
-"traefik.http.services.service1.loadbalancer.passhostheader": "true",
-"traefik.http.services.service1.loadbalancer.responseforwarding.flushinterval": "foobar",
-"traefik.http.services.service1.loadbalancer.sticky": "true",
-"traefik.http.services.service1.loadbalancer.sticky.cookie.httponly": "true",
-"traefik.http.services.service1.loadbalancer.sticky.cookie.name": "foobar",
-"traefik.http.services.service1.loadbalancer.sticky.cookie.secure": "true",
-"traefik.http.services.service1.loadbalancer.server.port": "foobar",
-"traefik.http.services.service1.loadbalancer.server.scheme": "foobar",
+"traefik.http.services.service01.loadbalancer.healthcheck.headers.name0": "foobar",
+"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.path": "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.passhostheader": "true",
+"traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval": "foobar",
+"traefik.http.services.service01.loadbalancer.sticky.cookie.httponly": "true",
+"traefik.http.services.service01.loadbalancer.sticky.cookie.name": "foobar",
+"traefik.http.services.service01.loadbalancer.sticky.cookie.secure": "true",
+"traefik.http.services.service01.loadbalancer.server.port": "foobar",
+"traefik.http.services.service01.loadbalancer.server.scheme": "foobar",
 "traefik.tcp.routers.tcprouter0.entrypoints": "foobar, foobar",
 "traefik.tcp.routers.tcprouter0.rule": "foobar",
 "traefik.tcp.routers.tcprouter0.service": "foobar",
-"traefik.tcp.routers.tcprouter0.tls": "true",
 "traefik.tcp.routers.tcprouter0.tls.certresolver": "foobar",
 "traefik.tcp.routers.tcprouter0.tls.domains[0].main": "foobar",
 "traefik.tcp.routers.tcprouter0.tls.domains[0].sans": "foobar, foobar",
@@ -175,7 +156,6 @@
 "traefik.tcp.routers.tcprouter1.entrypoints": "foobar, foobar",
 "traefik.tcp.routers.tcprouter1.rule": "foobar",
 "traefik.tcp.routers.tcprouter1.service": "foobar",
-"traefik.tcp.routers.tcprouter1.tls": "true",
 "traefik.tcp.routers.tcprouter1.tls.certresolver": "foobar",
 "traefik.tcp.routers.tcprouter1.tls.domains[0].main": "foobar",
 "traefik.tcp.routers.tcprouter1.tls.domains[0].sans": "foobar, foobar",
@@ -183,7 +163,5 @@
 "traefik.tcp.routers.tcprouter1.tls.domains[1].sans": "foobar, foobar",
 "traefik.tcp.routers.tcprouter1.tls.options": "foobar",
 "traefik.tcp.routers.tcprouter1.tls.passthrough": "true",
-"traefik.tcp.services.tcpservice0.loadbalancer.server.port": "foobar",
-"traefik.tcp.services.tcpservice0.loadbalancer.terminationDelay": "100",
-"traefik.tcp.services.tcpservice1.loadbalancer.server.port": "foobar"
-"traefik.tcp.services.tcpservice1.loadbalancer.terminationDelay": "100",
+"traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay": "42",
+"traefik.tcp.services.tcpservice01.loadbalancer.server.port": "foobar",
diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go
index a627ce621..aa63da133 100644
--- a/pkg/config/dynamic/middlewares.go
+++ b/pkg/config/dynamic/middlewares.go
@@ -355,7 +355,13 @@ type Retry struct {
 
 // StripPrefix holds the StripPrefix configuration.
 type StripPrefix struct {
-	Prefixes []string `json:"prefixes,omitempty" toml:"prefixes,omitempty" yaml:"prefixes,omitempty"`
+	Prefixes   []string `json:"prefixes,omitempty" toml:"prefixes,omitempty" yaml:"prefixes,omitempty"`
+	ForceSlash bool     `json:"forceSlash,omitempty" toml:"forceSlash,omitempty" yaml:"forceSlash,omitempty"` // Deprecated
+}
+
+// SetDefaults Default values for a StripPrefix.
+func (s *StripPrefix) SetDefaults() {
+	s.ForceSlash = true
 }
 
 // +k8s:deepcopy-gen=true
diff --git a/pkg/middlewares/addprefix/add_prefix.go b/pkg/middlewares/addprefix/add_prefix.go
index 655e874a8..934b7e0ae 100644
--- a/pkg/middlewares/addprefix/add_prefix.go
+++ b/pkg/middlewares/addprefix/add_prefix.go
@@ -41,23 +41,35 @@ func New(ctx context.Context, next http.Handler, config dynamic.AddPrefix, name
 	return result, nil
 }
 
-func (ap *addPrefix) GetTracingInformation() (string, ext.SpanKindEnum) {
-	return ap.name, tracing.SpanKindNoneEnum
+func (a *addPrefix) GetTracingInformation() (string, ext.SpanKindEnum) {
+	return a.name, tracing.SpanKindNoneEnum
 }
 
-func (ap *addPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
-	logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), ap.name, typeName))
+func (a *addPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+	logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), a.name, typeName))
 
 	oldURLPath := req.URL.Path
-	req.URL.Path = ap.prefix + req.URL.Path
+	req.URL.Path = ensureLeadingSlash(a.prefix + req.URL.Path)
 	logger.Debugf("URL.Path is now %s (was %s).", req.URL.Path, oldURLPath)
 
 	if req.URL.RawPath != "" {
 		oldURLRawPath := req.URL.RawPath
-		req.URL.RawPath = ap.prefix + req.URL.RawPath
+		req.URL.RawPath = ensureLeadingSlash(a.prefix + req.URL.RawPath)
 		logger.Debugf("URL.RawPath is now %s (was %s).", req.URL.RawPath, oldURLRawPath)
 	}
 	req.RequestURI = req.URL.RequestURI()
 
-	ap.next.ServeHTTP(rw, req)
+	a.next.ServeHTTP(rw, req)
+}
+
+func ensureLeadingSlash(str string) string {
+	if str == "" {
+		return str
+	}
+
+	if str[0] == '/' {
+		return str
+	}
+
+	return "/" + str
 }
diff --git a/pkg/middlewares/stripprefix/strip_prefix.go b/pkg/middlewares/stripprefix/strip_prefix.go
index b8008689a..1a0fd8bbc 100644
--- a/pkg/middlewares/stripprefix/strip_prefix.go
+++ b/pkg/middlewares/stripprefix/strip_prefix.go
@@ -20,18 +20,20 @@ const (
 
 // stripPrefix is a middleware used to strip prefix from an URL request.
 type stripPrefix struct {
-	next     http.Handler
-	prefixes []string
-	name     string
+	next       http.Handler
+	prefixes   []string
+	forceSlash bool // TODO Must be removed (breaking), the default behavior must be forceSlash=false
+	name       string
 }
 
 // New creates a new strip prefix middleware.
 func New(ctx context.Context, next http.Handler, config dynamic.StripPrefix, name string) (http.Handler, error) {
 	log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)).Debug("Creating middleware")
 	return &stripPrefix{
-		prefixes: config.Prefixes,
-		next:     next,
-		name:     name,
+		prefixes:   config.Prefixes,
+		forceSlash: config.ForceSlash,
+		next:       next,
+		name:       name,
 	}, nil
 }
 
@@ -42,9 +44,9 @@ func (s *stripPrefix) GetTracingInformation() (string, ext.SpanKindEnum) {
 func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	for _, prefix := range s.prefixes {
 		if strings.HasPrefix(req.URL.Path, prefix) {
-			req.URL.Path = getPrefixStripped(req.URL.Path, prefix)
+			req.URL.Path = s.getPrefixStripped(req.URL.Path, prefix)
 			if req.URL.RawPath != "" {
-				req.URL.RawPath = getPrefixStripped(req.URL.RawPath, prefix)
+				req.URL.RawPath = s.getPrefixStripped(req.URL.RawPath, prefix)
 			}
 			s.serveRequest(rw, req, strings.TrimSpace(prefix))
 			return
@@ -59,10 +61,25 @@ func (s *stripPrefix) serveRequest(rw http.ResponseWriter, req *http.Request, pr
 	s.next.ServeHTTP(rw, req)
 }
 
-func getPrefixStripped(s, prefix string) string {
-	return ensureLeadingSlash(strings.TrimPrefix(s, prefix))
+func (s *stripPrefix) getPrefixStripped(urlPath, prefix string) string {
+	if s.forceSlash {
+		// Only for compatibility reason with the previous behavior,
+		// but the previous behavior is wrong.
+		// This needs to be removed in the next breaking version.
+		return "/" + strings.TrimPrefix(strings.TrimPrefix(urlPath, prefix), "/")
+	}
+
+	return ensureLeadingSlash(strings.TrimPrefix(urlPath, prefix))
 }
 
 func ensureLeadingSlash(str string) string {
-	return "/" + strings.TrimPrefix(str, "/")
+	if str == "" {
+		return str
+	}
+
+	if str[0] == '/' {
+		return str
+	}
+
+	return "/" + str
 }
diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go
index 4024ac4f0..4feda225e 100644
--- a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go
+++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go
@@ -60,12 +60,12 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request)
 
 			req.Header.Add(stripprefix.ForwardedPrefixHeader, prefix)
 
-			req.URL.Path = strings.Replace(req.URL.Path, prefix, "", 1)
+			req.URL.Path = ensureLeadingSlash(strings.Replace(req.URL.Path, prefix, "", 1))
 			if req.URL.RawPath != "" {
-				req.URL.RawPath = req.URL.RawPath[len(prefix):]
+				req.URL.RawPath = ensureLeadingSlash(req.URL.RawPath[len(prefix):])
 			}
 
-			req.RequestURI = ensureLeadingSlash(req.URL.RequestURI())
+			req.RequestURI = req.URL.RequestURI()
 			s.next.ServeHTTP(rw, req)
 			return
 		}
@@ -75,5 +75,13 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request)
 }
 
 func ensureLeadingSlash(str string) string {
-	return "/" + strings.TrimPrefix(str, "/")
+	if str == "" {
+		return str
+	}
+
+	if str[0] == '/' {
+		return str
+	}
+
+	return "/" + str
 }


Test patch:
diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go
index 63ac2f48f..fbfece11e 100644
--- a/pkg/config/label/label_test.go
+++ b/pkg/config/label/label_test.go
@@ -368,6 +368,7 @@ func TestDecodeConfiguration(t *testing.T) {
 							"foobar",
 							"fiibar",
 						},
+						ForceSlash: true,
 					},
 				},
 				"Middleware18": {
@@ -771,6 +772,7 @@ func TestEncodeConfiguration(t *testing.T) {
 							"foobar",
 							"fiibar",
 						},
+						ForceSlash: true,
 					},
 				},
 				"Middleware18": {
@@ -1091,6 +1093,7 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Middlewares.Middleware15.ReplacePathRegex.Replacement":                       "foobar",
 		"traefik.HTTP.Middlewares.Middleware16.Retry.Attempts":                                     "42",
 		"traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes":                               "foobar, fiibar",
+		"traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash":                             "true",
 		"traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex":                             "foobar, fiibar",
 		"traefik.HTTP.Middlewares.Middleware19.Compress":                                           "true",
 
diff --git a/pkg/middlewares/addprefix/add_prefix_test.go b/pkg/middlewares/addprefix/add_prefix_test.go
index 1d130a083..cca4bd19e 100644
--- a/pkg/middlewares/addprefix/add_prefix_test.go
+++ b/pkg/middlewares/addprefix/add_prefix_test.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/containous/traefik/v2/pkg/config/dynamic"
 	"github.com/containous/traefik/v2/pkg/testhelpers"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -47,7 +46,6 @@ func TestNewAddPrefix(t *testing.T) {
 }
 
 func TestAddPrefix(t *testing.T) {
-	logrus.SetLevel(logrus.DebugLevel)
 	testCases := []struct {
 		desc            string
 		prefix          dynamic.AddPrefix
@@ -61,6 +59,12 @@ func TestAddPrefix(t *testing.T) {
 			path:         "/b",
 			expectedPath: "/a/b",
 		},
+		{
+			desc:         "Works with missing leading slash",
+			prefix:       dynamic.AddPrefix{Prefix: "a"},
+			path:         "/",
+			expectedPath: "/a/",
+		},
 		{
 			desc:            "Works with a raw path",
 			prefix:          dynamic.AddPrefix{Prefix: "/a"},
diff --git a/pkg/middlewares/stripprefix/strip_prefix_test.go b/pkg/middlewares/stripprefix/strip_prefix_test.go
index 449720902..13a5aedb2 100644
--- a/pkg/middlewares/stripprefix/strip_prefix_test.go
+++ b/pkg/middlewares/stripprefix/strip_prefix_test.go
@@ -31,6 +31,17 @@ func TestStripPrefix(t *testing.T) {
 			expectedStatusCode: http.StatusOK,
 			expectedPath:       "/noprefixes",
 		},
+		{
+			desc: "wildcard (.*) requests (ForceSlash)",
+			config: dynamic.StripPrefix{
+				Prefixes:   []string{"/"},
+				ForceSlash: true,
+			},
+			path:               "/",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "/",
+			expectedHeader:     "/",
+		},
 		{
 			desc: "wildcard (.*) requests",
 			config: dynamic.StripPrefix{
@@ -38,9 +49,20 @@ func TestStripPrefix(t *testing.T) {
 			},
 			path:               "/",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "/",
+			expectedPath:       "",
 			expectedHeader:     "/",
 		},
+		{
+			desc: "prefix and path matching (ForceSlash)",
+			config: dynamic.StripPrefix{
+				Prefixes:   []string{"/stat"},
+				ForceSlash: true,
+			},
+			path:               "/stat",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "/",
+			expectedHeader:     "/stat",
+		},
 		{
 			desc: "prefix and path matching",
 			config: dynamic.StripPrefix{
@@ -48,9 +70,20 @@ func TestStripPrefix(t *testing.T) {
 			},
 			path:               "/stat",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "/",
+			expectedPath:       "",
 			expectedHeader:     "/stat",
 		},
+		{
+			desc: "path prefix on exactly matching path (ForceSlash)",
+			config: dynamic.StripPrefix{
+				Prefixes:   []string{"/stat/"},
+				ForceSlash: true,
+			},
+			path:               "/stat/",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "/",
+			expectedHeader:     "/stat/",
+		},
 		{
 			desc: "path prefix on exactly matching path",
 			config: dynamic.StripPrefix{
@@ -58,7 +91,7 @@ func TestStripPrefix(t *testing.T) {
 			},
 			path:               "/stat/",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "/",
+			expectedPath:       "",
 			expectedHeader:     "/stat/",
 		},
 		{
@@ -101,6 +134,17 @@ func TestStripPrefix(t *testing.T) {
 			expectedPath:       "/us",
 			expectedHeader:     "/stat",
 		},
+		{
+			desc: "later prefix matching (ForceSlash)",
+			config: dynamic.StripPrefix{
+				Prefixes:   []string{"/mismatch", "/stat"},
+				ForceSlash: true,
+			},
+			path:               "/stat",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "/",
+			expectedHeader:     "/stat",
+		},
 		{
 			desc: "later prefix matching",
 			config: dynamic.StripPrefix{
@@ -108,7 +152,7 @@ func TestStripPrefix(t *testing.T) {
 			},
 			path:               "/stat",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "/",
+			expectedPath:       "",
 			expectedHeader:     "/stat",
 		},
 		{
@@ -162,12 +206,15 @@ func TestStripPrefix(t *testing.T) {
 			assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.")
 			assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ForwardedPrefixHeader)
 
-			expectedURI := test.expectedPath
+			expectedRequestURI := test.expectedPath
 			if test.expectedRawPath != "" {
 				// go HTTP uses the raw path when existent in the RequestURI
-				expectedURI = test.expectedRawPath
+				expectedRequestURI = test.expectedRawPath
+			}
+			if test.expectedPath == "" {
+				expectedRequestURI = "/"
 			}
-			assert.Equal(t, expectedURI, requestURI, "Unexpected request URI.")
+			assert.Equal(t, expectedRequestURI, requestURI, "Unexpected request URI.")
 		})
 	}
 }
diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go
index 297fa7192..6e0293083 100644
--- a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go
+++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go
@@ -31,42 +31,66 @@ func TestStripPrefixRegex(t *testing.T) {
 			expectedPath:       "/a/test",
 		},
 		{
-			path:               "/a/test",
+			path:               "/a/test/",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "/a/test",
+			expectedPath:       "/a/test/",
+		},
+		{
+			path:               "/a/api/",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "",
+			expectedHeader:     "/a/api/",
 		},
 		{
 			path:               "/a/api/test",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "test",
+			expectedPath:       "/test",
+			expectedHeader:     "/a/api/",
+		},
+		{
+			path:               "/a/api/test/",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "/test/",
 			expectedHeader:     "/a/api/",
 		},
 		{
 			path:               "/b/api/",
 			expectedStatusCode: http.StatusOK,
+			expectedPath:       "",
 			expectedHeader:     "/b/api/",
 		},
+		{
+			path:               "/b/api",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "/b/api",
+		},
 		{
 			path:               "/b/api/test1",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "test1",
+			expectedPath:       "/test1",
 			expectedHeader:     "/b/api/",
 		},
 		{
 			path:               "/b/api2/test2",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "test2",
+			expectedPath:       "/test2",
 			expectedHeader:     "/b/api2/",
 		},
 		{
 			path:               "/c/api/123/",
 			expectedStatusCode: http.StatusOK,
+			expectedPath:       "",
 			expectedHeader:     "/c/api/123/",
 		},
+		{
+			path:               "/c/api/123",
+			expectedStatusCode: http.StatusOK,
+			expectedPath:       "/c/api/123",
+		},
 		{
 			path:               "/c/api/123/test3",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "test3",
+			expectedPath:       "/test3",
 			expectedHeader:     "/c/api/123/",
 		},
 		{
@@ -77,8 +101,8 @@ func TestStripPrefixRegex(t *testing.T) {
 		{
 			path:               "/a/api/a%2Fb",
 			expectedStatusCode: http.StatusOK,
-			expectedPath:       "a/b",
-			expectedRawPath:    "a%2Fb",
+			expectedPath:       "/a/b",
+			expectedRawPath:    "/a%2Fb",
 			expectedHeader:     "/a/api/",
 		},
 	}
@@ -88,11 +112,12 @@ func TestStripPrefixRegex(t *testing.T) {
 		t.Run(test.path, func(t *testing.T) {
 			t.Parallel()
 
-			var actualPath, actualRawPath, actualHeader string
+			var actualPath, actualRawPath, actualHeader, requestURI string
 			handlerPath := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				actualPath = r.URL.Path
 				actualRawPath = r.URL.RawPath
 				actualHeader = r.Header.Get(stripprefix.ForwardedPrefixHeader)
+				requestURI = r.RequestURI
 			})
 			handler, err := New(context.Background(), handlerPath, testPrefixRegex, "foo-strip-prefix-regex")
 			require.NoError(t, err)
@@ -106,6 +131,18 @@ func TestStripPrefixRegex(t *testing.T) {
 			assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
 			assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.")
 			assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", stripprefix.ForwardedPrefixHeader)
+
+			if test.expectedPath != test.path {
+				expectedRequestURI := test.expectedPath
+				if test.expectedRawPath != "" {
+					// go HTTP uses the raw path when existent in the RequestURI
+					expectedRequestURI = test.expectedRawPath
+				}
+				if test.expectedPath == "" {
+					expectedRequestURI = "/"
+				}
+				assert.Equal(t, expectedRequestURI, requestURI, "Unexpected request URI.")
+			}
 		})
 	}
 }