smithy4s icon indicating copy to clipboard operation
smithy4s copied to clipboard

Adding a base path prefix to API routes causes UI not to load

Open kyri-petrou opened this issue 3 years ago • 5 comments

Hi everyone 👋

There is probably an easy solution to this but I can't seem to figure it out.

When the server is behind a proxy which routes requests with a specific prefix to the root of the server (i.e., the server will not see the prefix in the request path), the Swagger UI no longer loads and fails with a 404 error. Note that the API routes still work as expected.

I was wondering, is there a setting where I can define a prefix to the API routes or to make the UI work based on the relative path instead of the absolute path?

Please let me know if my explanation is unclear and I'll put together an example

kyri-petrou avatar Sep 07 '22 04:09 kyri-petrou

Okay it seems I was mistaken, it's not just a 404 error. Let's assume that the docs endpoint on the server are served on /docs, and that the proxy routes /admin/docs to the server as /docs.

When opening https://some-domain.com/admin/docs on the browser, the server issues a 302 response and then the browser changes to https://some-domain.com/docs which then returns a 404.

More interesting is that if we add index.html to the path above (i.e., https://some-domain.com/admin/docs/index.html), the page loads and displays an error instead: image

kyri-petrou avatar Sep 07 '22 05:09 kyri-petrou

Let's make sure we understand each other. In the following, we can assume proxy is something like nginx and app is the app you build with smithy4s (I'd assume a http4s server).

  1. you have a proxy in front of the app
  2. when you hit /admin/* you expect your request to be redirected to the _app_
  3. as such, a request on the proxy at /admin/docs should result in a request to the app at /docs

Is that what you mean?


if we assume the above is true, I did some testing. set up an nginx locally along with an app and here's what I see. I can confirm what you see:

Screen Shot 2022-09-07 at 14 45 44

Hitting: http://localhost/admin/docs sends me to my http://app:8080/docs correctly, but the response is a redirect, which is expected:

curl -v 'http://localhost/admin/docs'
*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /admin/docs HTTP/1.1
> Host: localhost
> User-Agent: curl/7.79.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Server: nginx/1.23.1
< Date: Wed, 07 Sep 2022 18:52:51 GMT
< Content-Length: 0
< Connection: keep-alive
< Location: /docs/index.html
< 
* Connection #0 to host localhost left intact

With the way this is currently implemented, I was not able to write a nginx configuration to do the redirecting/rewriting correctly. I will work on an improvement.

Thanks for reporting this

daddykotex avatar Sep 07 '22 19:09 daddykotex

Is that what you mean?

Yes that is correct

Thank you very much for looking into this!

kyri-petrou avatar Sep 07 '22 20:09 kyri-petrou

So after further investigation, I've got a solution that works (at least using nginx). To reproduce your issue, I've created the following setup.

Using docker compose, I build:

  1. my app ( a simple http4s hello world build with smithy4s). This app runs in a container and exposes port 8080. so I can do curl http://localhost:8080/docs and get a 302 Location /docs/index.html. This is expected and it's fine by itself.
  2. a proxy using nginx. this proxy routes all traffic on /admin/ to `http://localhost:8080 (config available below)

Here is the docker-compose:

version: "3.9"
services:
  web:
    image: "app:dev-SNAPSHOT"
    ports:
      - "8080:8080"
  proxy:
    build: ./nginx/
    ports:
      - "80:80"

The content of ./nginx is as follow:

Dockerfile:

FROM nginx:1.23.1-alpine
COPY default.conf /etc/nginx/conf.d/default.conf

default.conf:

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    location /admin/ {
        proxy_pass http://web:8080/; # web matches the name of the other container in docker-compose
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}

With all that, my reproduction is complete.


Now, it is my belief that if you run a proxy in front of an app, you're kind of taking the responsibility of rewriting things that you play with. In this case, we're routing a different path so I would expect nginx to rewrite the redirect response, otherwise it can't work. So I looked into it. It turns out that proxy_pass automatically rewrite such header using a default proxy_redirect configuration. So if the header was Location http://web:8080/docs/index.html nginx would rewrite it.

So I gave that a spin, I've adjusted the implementation of the endpoint to return http://web:8080/docs/index.html instead of /docs/index.html and gave it a spin:

curl -v 'http://localhost/admin/docs'
*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /admin/docs HTTP/1.1
> Host: localhost
> User-Agent: curl/7.79.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Server: nginx/1.23.1
< Date: Thu, 08 Sep 2022 18:24:54 GMT
< Content-Length: 0
< Location: http://localhost/admin/docs/index.html
< Connection: keep-alive
< 

Success. So the fix is easy: rewrite the redirect to have full URL instead of a relative path.

Turns out it's not that easy with http4s. I've discussed with maintainers and we'll come up with a good way to do that.

In the meantime, I think we can just avoid the redirect and return the index.html on the call to /docs and that will work.

Also, the path to the specification will change from root relative /path/to/spec.yml to document relative: ./path/to/specs.yml so that fixes that second issue where the spec could not be fetched.

daddykotex avatar Sep 08 '22 18:09 daddykotex

Turns out returning the index.html content breaks all the document relative link in the html itself so it's not viable. We'll update the Docs endpoint implementation when https://github.com/http4s/http4s/issues/6658 is resolved

daddykotex avatar Sep 08 '22 19:09 daddykotex

@daddykotex are we able to close this issue now ?

Baccata avatar Sep 22 '23 09:09 Baccata

After refreshing my memory, I don't think we can. If I recall correctly, I just want my http4s implementation to do the following:

get a request on http://localhost:8080/hello return a redirect to http://localhost:8080/docs/hello not /docs/hello

I was unable at the time to implement that, maybe I could revisit and pull it off

daddykotex avatar Sep 22 '23 14:09 daddykotex