juhaku / utoipa

Simple, Fast, Code first and Compile time generated OpenAPI documentation for Rust
Apache License 2.0
2.2k stars 168 forks source link

Is there any way to configure utoipa behind reverse proxy? #842

Open qrilka opened 8 months ago

qrilka commented 8 months ago

OpenAPI supports servers but reverse proxy could be configured independently for a particular service sitting behind that proxy. utoipa has support for servers but it looks like the trait Modify is static. Is there some way to use X-Forwarded headers like for example it's done in https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/1801 ?

plang-arista commented 6 months ago

Hi, I've also run into the same issue, but found a workaround.

My setup: Axum server running behind Reverse Proxy, with URL rewrite: "/api" -> "/"

struct PathPrefixAddon;

impl Modify for PathPrefixAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        match env::var("PATH_PREFIX") {
            Ok(prefix) => openapi.servers = Some(vec![Server::new(prefix)]),
            Err(_) => ()
        }
    }
}

async fn main() {
    #[derive(OpenApi)]
    #[openapi(modifiers(&PathPrefixAddon))]
    struct ApiDoc;

    let app = Router::new()
        .merge(SwaggerUi::new("/docs").config(Config::new(["openapi.json"])).url("/docs/openapi.json", ApiDoc::openapi()));
}

The PathPrefixAddon tricks utoipa to use "/{prefix}" to access the server, which essentially adds a prefix to every endpoint.

The .config(Config::new(["openapi.json"])) is also important so that the Swagger UI would look for the openapi.json with a path relative to the /docs.

Note that there are other pitfalls to this setup:

lpfy commented 5 months ago

tricks utoipa to use "/{prefix}" to access the server, which essentially adds a prefix to every endpoint.

Hi @plang-arista, thanks for your workaround. I can confirm it also works for the Swagger UI part when loading openapi.json on Actix-web, the next problem I am facing is the "Try it out" part still using the wrong endpoint.

for example, my server is http://www.abcd.com/, set Apache reverse proxy to http://www.abcd.com/rusttest/ for Rust Actix-web The swagger UI URL becomes http://www.abcd.com/rusttest/swagger/ The actual API endpoint is http://www.abcd.com/rusttest/api/xxxxABC The "Try it out" in utoipa swagger UI send request to http://www.abcd.com/api/xxxxABC, the prefix "/rusttest" is missing.

Could you please help to see what's wrong in my code? thanks heaps

use dotenv::dotenv;
use actix_cors::Cors;
use actix_web::{App, HttpServer, web::Data};
use std::{error::Error, env};
use utoipa::{OpenApi, Modify, openapi::server::Server};
use utoipa_swagger_ui::{SwaggerUi, Config};

struct PathPrefixAddon;

impl Modify for PathPrefixAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        match env::var("PATH_PREFIX") {
            Ok(prefix) => openapi.servers = Some(vec![Server::new(prefix)]),
            Err(_) => ()
        }
    }
}

async fn main() -> Result<(), Box<dyn Error>> {
    dotenv().ok();
    //Because Apache reverse proxy configuration, 
    //environment variable need to be set in .env file for PATH_PREFIX

    //Auto-generate API documentation using utoipa and utoipa-swagger-ui 
    #[derive(OpenApi)]
    #[openapi(
        modifiers(&PathPrefixAddon),
        paths(
            get_pages,
        ), 
        components(schemas(
            Page,
        ))
    )]
    struct ApiDoc;

    let openapi = ApiDoc::openapi();

    HttpServer::new(move || {
        App::new()
            .wrap(Cors::permissive()) // Add this line to enable CORS for all origins
            .service(
                //Because Apache reverse proxy configuration, we have to use PathPrefixAddon
                //It tricks utoipa to use "/{prefix}" to access the server, which essentially adds a prefix to every endpoint.
                SwaggerUi::new("/swagger/{_:.*}").config(Config::new(["openapi.json"])).url("/swagger/openapi.json", openapi.clone()),
            )
            .configure(init_routes)   // Route being handled by routes.rs
    })
    .bind((CONFIG_PARAMETERS.webserver_host.to_string(), CONFIG_PARAMETERS.webserver_port,))?
    .run()
    .await?;
    Ok(())
}
plang-arista commented 5 months ago

tricks utoipa to use "/{prefix}" to access the server, which essentially adds a prefix to every endpoint.

Hi @plang-arista, thanks for your workaround. I can confirm it also works for the Swagger UI part when loading openapi.json on Actix-web, the next problem I am facing is the "Try it out" part still using the wrong endpoint.

Hi, this is weird, this should work. Can you add logging that you have set your PATH_PREFIX env var to /rusttest/ as well? You can also just hard-code that value for your test, to be on the safe side.

In the swagger page you should see a new Drop-Down select, labelled "Servers" with single value /rusttest/. This is how swagger-ui would know that your /api/xxxxABC api calls would need to be made against /rusttest/ server on the same host, essentially becoming http://www.abcd.com/rusttest/api/xxxxABC.

If that variable is also properly set up, I'm not sure how to go on.

lpfy commented 5 months ago

tricks utoipa to use "/{prefix}" to access the server, which essentially adds a prefix to every endpoint.

Hi @plang-arista, thanks for your workaround. I can confirm it also works for the Swagger UI part when loading openapi.json on Actix-web, the next problem I am facing is the "Try it out" part still using the wrong endpoint.

Hi, this is weird, this should work. Can you add logging that you have set your PATH_PREFIX env var to /rusttest/ as well? You can also just hard-code that value for your test, to be on the safe side.

In the swagger page you should see a new Drop-Down select, labelled "Servers" with single value /rusttest/. This is how swagger-ui would know that your /api/xxxxABC api calls would need to be made against /rusttest/ server on the same host, essentially becoming http://www.abcd.com/rusttest/api/xxxxABC.

If that variable is also properly set up, I'm not sure how to go on.

Thanks for the reply. For the current swagger page, it didn't show Drop-Down select, labelled "Servers" with single value /rusttest/. So might be env var problem. As the server is an internal server behind the firewall, so I could not test it over the weekend at home. Will look into this next Monday.

lpfy commented 5 months ago

tricks utoipa to use "/{prefix}" to access the server, which essentially adds a prefix to every endpoint.

Hi @plang-arista, thanks for your workaround. I can confirm it also works for the Swagger UI part when loading openapi.json on Actix-web, the next problem I am facing is the "Try it out" part still using the wrong endpoint.

Hi, this is weird, this should work. Can you add logging that you have set your PATH_PREFIX env var to /rusttest/ as well? You can also just hard-code that value for your test, to be on the safe side.

In the swagger page you should see a new Drop-Down select, labelled "Servers" with single value /rusttest/. This is how swagger-ui would know that your /api/xxxxABC api calls would need to be made against /rusttest/ server on the same host, essentially becoming http://www.abcd.com/rusttest/api/xxxxABC.

If that variable is also properly set up, I'm not sure how to go on.

Hi @plang-arista, after I correctly set PATH_PREFIXenv var to /rusttest/, Swagger UI is working properly now. thanks for your help.

qrilka commented 4 months ago

The workaround given will not fix the problem with trailing slash redirect from https://github.com/juhaku/utoipa/blob/4d798bc3e79a0991460c14b657b03bd17d08d7df/utoipa-swagger-ui/src/axum.rs#L52

thus if the API is available under prefix /service as /api it will work as /service/api/ but /service/api will give a bad redirect to /api

plang-arista commented 4 months ago

The workaround given will not fix the problem with trailing slash redirect from

https://github.com/juhaku/utoipa/blob/4d798bc3e79a0991460c14b657b03bd17d08d7df/utoipa-swagger-ui/src/axum.rs#L52

thus if the API is available under prefix /service as /api it will work as /service/api/ but /service/api will give a bad redirect to /api

Yeah, I've covered that in:

Axum / Utoipa might respond 303 for requests to /docs to redirect it to Location /docs/. In your reverse-proxy you also need to setup redirect translation. E.g.: in Nginx I had to add proxy_redirect / /api/; to prepend the proper prefix.

This is what I have in my nginx reverse proxy to handle that:

server {
  location /api {
    rewrite /api/(.*) /$1 break;
    proxy_pass        http://backend-proxied:5000;
    proxy_redirect    / /api/;
    proxy_buffering   off;
    proxy_set_header  Host $host;
  }
}