swagger-ui icon indicating copy to clipboard operation
swagger-ui copied to clipboard

swagger-ui: "Try it Now" functionality stripping path prefixes when run behind nginx-ingress reverse proxy in Kubernetes/K8S.

Open ghost opened this issue 5 years ago • 7 comments

Q&A (please complete the following information)

  • OS: Linux (Ubuntu x86_64 LTS 20.04).
  • Browser: Firefox.
  • Version: v84.0.1 64-bit.
  • Method of installation: Helm v3.x in K8S (though trying the manual "dist/index.html" approach yields similar results).
  • Swagger-UI version: v3.24.3 (pre-built Docker container), v3.40.0 (plain old "dist/index.html" approach).
  • Swagger/OpenAPI version: OpenAPI 3.0.

Content & configuration

  • Setup an instance of an application exposed through an nginx-ingress rule in K8S. Sample ingress YAML included below. In this example, the host is roadrunner.acme.co, and the service is exposed on http://roadrunner.acme.com/meep/v1.
    • Note: I've found a few other projects on GitHub or via Google searches where the solution is "just expose your app on hostname/, rather than a custom path invoking nginx rewrites such as hostname/meep/v1.
    • This is not an option, as that would assume only a single application is exposed via the ingress controller. In a typical/production use case, each micro-service in behind the ingress-control would get its own dedicated base path, like meep/v1 in my example).
  • Download and extract the contents of the "plain old HTML/JS" archive.
  • Modify the dist/index.html file so the URL field points to the OpenAPI spec of my service (i.e. http://roadrunner.acme.co/meep/v1/openapi.json; the service is a simple Python Flash+Swagger example that auto-exposes the openapi.json endpoint).
  • I am able to access the Swagger-UI web UI example, interact with it, etc. However, if I attempt to use the "Try It Now" ==> "Execute" functionality, the API calls to the live/running app fail due to a "Network error".
  • Upon closer inspection, I see that the "Execute" calls are attempting to communicate with http://roadrunner.acme.co/v1/ rather than http://roadrunner.acme.co/meep/v1 (i.e. the re-written portion of the URL is absent).
  • I was able to arrive at the same result by deploying swagger-ui into the cluster directly via a third-party helm chart.

Example ingress:

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: sample-ingress-meep-meep
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - http:
        paths:
          - backend:
              serviceName: velocitus-good-with-ketchup-ius
              servicePort: 80
            path: /meep/(v1/.*)

Example Swagger/OpenAPI definition: N/A.

Swagger-UI configuration options:

window.onload = function() {
      // Begin Swagger UI call region
      const ui = SwaggerUIBundle({
        url: "http://roadrunner.acme.co/meep/v1/openapi.json",
        dom_id: '#swagger-ui',
        deepLinking: true,
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIStandalonePreset
        ],
        plugins: [
          SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout"
      })
      // End Swagger UI call region

      window.ui = ui
    }

Describe the bug you're encountering

  • The "Try It Now" ==> "Execute" functionality does not work, but access to the HTML app in general works.

To reproduce...

Example described above.

Expected behavior

  • The "Try It Now" ==> "Execute" functionality uses the exact same URL (i.e. hostname plus base path) that is used to access the running OpenAPI-enabled service (maybe using X-Forwarded-For or additional parameters supplied to the ingress-controller, so that re-writes don't break functionality).

Additional context or thoughts

There's a hack I've figured out in order to make the "Try It Now" ==> "Execute" functionality work, but the downside (severe), is that I need to supply the external hostname to the swagger-ui app at chart installation time (i.e. when I install swagger-ui, it needs to be provided with the string http://roadrunner.acme.co/meep/v1 at installation time). This isn't a permanent/viable solution though, as various users may "see" a different hostname and base path when accessing this micro-service (e.g. if it's exposed via multiple domains with a common load balancer, or if some people attempt to access it via IP address rather than this specific host name). It also prevents 100% end-to-end automated installation, as someone needs to supply the external FQDN + base path at chart installation time.

Hack-y workaround:

  • Deploy an OpenAPI-enabled microservice, expose it via an ingress rule (below). Can be deployed via kubectl apply --namespace=acme-ns -f ingress-meep.yaml.

ingress-meep.yaml

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: sample-ingress-meep-meep
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - http:
        paths:
          - backend:
              serviceName: velocitus-good-with-ketchup-ius
              servicePort: 80
            path: /meep/(v1/.*)
  • Pre-create an ingress rule for swagger-ui (below). Can be deployed via kubectl apply --namespace=acme-ns -f ingress-demo.yaml.

ingress-demo.yaml

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: sample-ingress-swaggerui-demo
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    nginx.ingress.kubernetes.io/enable-cors: "true"
spec:
  rules:
    - http:
        paths:
          - backend:
              serviceName: swagger-demo-swaggerui
              servicePort: 8080
            path: /swagger-demo/(.*)
  • Using the following commands to deploy the chart.
# Make chart "visible" to Helm.
helm repo add cetic https://cetic.github.io/helm-charts
helm repo update

# Delete old copy if present.
helm delete --namespace=acme-ns swagger-demo

# Install chart.
helm install --namespace=acme-ns swagger-demo cetic/swagger-ui \
    --set swaggerui.jsonUrl="http://velocitus-good-with-ketchup-ius.acme-ns.svc.cluster.local/v1/openapi.json" \
    --set swaggerui.server.url="http://roadrunner.acme.co/meep/v1/"
  • Navigate to <http://roadrunner.acme.co/swagger-demo/> in my browser. Application loads and is visible in my browser.
  • Change the "Servers" via the drop-down UI menu (top-left) from v1 to http://roadrunner.acme.co/meep/v1. Both the general UI features, and the live debug "Try It Now" features work as expected.

Possibly related issues (external projects)

  • https://github.com/RicoSuter/NSwag/issues/3192
  • https://github.com/RicoSuter/NSwag/issues/1717
  • https://github.com/noirbizarre/flask-restplus/issues/310

ghost avatar Jan 15 '21 18:01 ghost

Hi! Not certain what version of Kubernetes you're using (I'm on Kubernetes 1.20.4 and NGINX Ingress controller v0.44.0),. Adding the nginx.ingress.kubernetes.io/x-forwarded-prefix annotation worked for me.

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sample-ingress-swaggerui-demo
  annotations:
    nginx.ingress.kubernetes.io/x-forwarded-prefix: "/swagger-demo"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    nginx.ingress.kubernetes.io/enable-cors: "true"
spec:
  rules:
    - http:
        paths:
          - backend:
              serviceName: swagger-demo-swaggerui
              servicePort: 8080
            path: /swagger-demo/(.*)

Note: I'm using networking.k8s.io/v1

edit:: Documentation for nginx.ingress.kubernetes.io/x-forwarded-prefix

DanSibbernsen avatar May 28 '21 05:05 DanSibbernsen

@DanSibbernsen What if you have multiple paths? You don't want to pass x-forwarded-prefix to other paths than swagger-demo-swaggerui.

80rian avatar Nov 22 '21 03:11 80rian

@80rian Unfortunately my solution doesn't cover that that (for the apps I maintain, we only have 1 path per service, and the only one I cared about for the x-forwarded-prefix was for Swagger). Per this comment, it seems you'd have to define multiple ingresses in order to have different x-forwarded-prefix values passed along.

DanSibbernsen avatar Nov 22 '21 07:11 DanSibbernsen

Gotcha. Thanks!

80rian avatar Nov 22 '21 07:11 80rian

Hi! I tried to pass different x-forwarded-prefix values to multiple backend paths on the ingress-nginx (v1.0.4) and Kubernetes (1.21.7) through the nginx.ingress.kubernetes.io/x-forwarded-prefix annotation as @DanSibbernsen's comment.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-demo
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$3
    nginx.ingress.kubernetes.io/x-forwarded-prefix: /$1
spec:
  rules:
  - http:
      paths:
      - path: /()()(.*)
        pathType: Prefix
        backend:
          service:
            name: web
            port:
              number: 80
      - path: /(XXXApi)(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: xxx-api
            port:
              number: 80
      - path: /(YYYApi)(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: yyy-api
            port:
              number: 80

happyincent avatar Jan 28 '22 08:01 happyincent

@happyincent Thank you very much!

ov-petrov avatar Jul 08 '22 15:07 ov-petrov

Someone help solve the problem!

Hello. I ran into a similar problem and google brought me to this thread. I am using kubernetes v1.23.6 and nginx-ingress-controller v1.3.1

If I use host-based routing using the address "api.[SomeDomain].com" then everything works fine and I can access the api and SwaggerUI. But when I try to use path-based Routing "api.[SomeDomain].com/test", then I constantly fail: When going to the address "api.[SomeDomain].com/test/swagger/index.html" I get a SwaggerUI page with the error: "API definition could not be loaded. The status of the response to the sampling error is 404 /swagger/v1/swagger.json". But if I go from the browser to: "api.[SomeDomain].com/test/swagger/v1/swagger.json" - I get normal json in response. Api requests at the address "api.[SomeDomain].com/test/..." also pass a good one. I tried many examples and the internet, but none solved my problem! ** Of course, instead of "[SomeDomain]" I use the real domain name, which is resolved through the global DNS :-)

The example suggested here fom @happyincent - also doesn't work for me.

Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: testapi-ingress
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$3
    nginx.ingress.kubernetes.io/x-forwarded-prefix: /$1
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.[SomeDomain].com
    secretName: "wildcard-tls-secret"
  rules:
  - host: api.[SomeDomain].com
    http:
      paths:
      - path: /(test)(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: testapi-svc
            port:
              number: 80

Error in SwaggerUi: image

MrAliev avatar Sep 11 '22 14:09 MrAliev

same here

goors avatar May 13 '23 14:05 goors