Fastapi: How does the /docs route redirect to the /openapi.json route?

Created on 24 Mar 2019  路  10Comments  路  Source: tiangolo/fastapi

Hello there!
I am running an nginx reverse proxy with multiple fastapi microservices with the use of docker-compose. I am trying to give access to each microservice's documentation by configuring a proxy_pass from route /docs/serviceName to the microservice's doc. For example, here's the nginx config:

    upstream configuration-service {
        server configuration-service:8001;
    }

    location /docs/configuration-service {
        proxy_pass http://configuration-service/docs;
    }

This proxy_pass works just fine, but my browser then tries to access http://localhost:8000/api/v1/openapi.json instead of http://localhost:8001/api/v1/openapi.json where the configuration-service is located.

A workaround that I found was to proxy_pass the /api/v1/openapi.json route to my configuration-service and it works, but once I start adding more services to my docker-compose, I will have to define a unique openapi_prefix for each and have 2 proxy_pass configurations in my nginx container for each.

Is there any other option?

Thank you!

question

Most helpful comment

I strongly support the recommendation to use Traefik - makes reverse proxying to docker services really, really easy and configurable. Also handles rate-limiting, round-robin/etc routing, route-level middleware, path conversions, ...

All 10 comments

I may be wrong, but I'm not aware of a redirect from /docs to /openapi.json, there is effectively /docs doing a xhr to get it but no redirect.
it comes from the openapi_url argument of FastAPI, might be worth looking how you serve that route, it looks more like a nginx order issue to me.

@euri10 When I check the network tab in chrome developper tools, I see a GET request to url/api/v1/openapi.json after I attempted a GET request to /docs

My issue isn't the route really, it's the IP that it's trying to hit.
Nginx container is exposed publicly on port 8000
Configuration-service container is only exposed on port 8001, but not open to public

The order of request goes:

  1. localhost:8000/docs/configuration-service
  2. localhost:8001/docs
  3. localhost:8000/api/v1/openapi.json

The /api/v1/openapi.json part of request 3 is configured inside the configuration-service with openapi_prefix so it knows what route to go to, but it's the host/port that's not going to the right spot. That's the reason why doing a second proxy_pass is a workaround to my issue.

Yes, I would suggest a better way (depends on point of view). I would use Traefik, integrated into Docker (and Docker Compose). And then set the rules of which service should take which path in labels in each service, instead of a lot of configurations that have to be updated and kept in sync in a separated Nginx service.

That's all already done and preconfigured for you to use, you can try one of the project generators. It takes 2 minutes to generate a directory with everything set up, and all integrated with Docker Compose: https://fastapi.tiangolo.com/project-generation/

I strongly support the recommendation to use Traefik - makes reverse proxying to docker services really, really easy and configurable. Also handles rate-limiting, round-robin/etc routing, route-level middleware, path conversions, ...

I'll assume you were able to solve your problem and I'll close this issue, but feel free to add more comments or create new issues.

hi @pgarneau . i have the same issue with openapi not working with nginx url re-write. can you share the code snippet as to how you solved the issue.

Hi @pgarneau , I'm facing same issue. could you please share the code snippet of workaround you implemented?

Quickfix: Add a new location that informs clients that the spec has moved permanently to where the API actually lives.

location ^~ /openapi.json {
  # or to where ever your API lives
  return 301 /api/openapi.json;
}

Logic:
The docs tell the client to get the API spec at /openapi.json and the client will faithfully request that path to get the spec. If Nginx sits between the client and the API - and the API lives at location ^~ /api/ - then Nginx will do it's usual matching and conclude that /openapi.json does not match /api/ and will hence not forward the request to fastAPI but instead try to otherwise resolve it.

Adding a location for that route specifically can patch this. You will get problems if you have multiple versions of an API or multiple independent APIs on the same server (all will request the same /openapi.json and you can't differentiate which one is requesting it). For that, I guess there might be an environment variable of some sort that allows changing where the docs look for the spec? Otherwise, this could be a nice feature to add :)

https://fastapi.tiangolo.com/advanced/extending-openapi/#the-normal-process

As part of the application object creation, a path operation for /openapi.json (or for whatever you set your openapi_url) is registered.

Basically you can change where the openapi.json is by overriding the openapi_url parameter as detailed in the help docs (link above). You can also override the /docs or /redocs endpoints with whatever you want (I did that using Nils de Bruins blog post instructions to restrict access to anyone not logged in using cookie-based tokens for the API docs).

@wshayes Yes, exactly. You will need some additional plumbing though to make it play nice with Nginx.

Consider


main.py

from fastapi import FastAPI
import uvicorn
from fastapi.openapi.utils import get_openapi


app = FastAPI(
    openapi_url="/v1/docs"
)


@app.get("/")
async def root():
    return {"message": "Hello World"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

and


server.conf

server {
  listen       80;
  server_name  localhost;

  location ^~ /api/v1/ {   
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_redirect off;
    proxy_buffering off;
    proxy_pass http://api:8000/;
  }
}

as well as the following way to bring it together


docker-compose.yml

version: '3'

services:
  nginx:
    image: nginx
    ports:
      - 80:80
    volumes:
      - ./server.conf:/etc/nginx/conf.d/default.conf
    depends_on: 
      - api

  api:
    image: python:3.7
    volumes:
      - ./main.py:/main.py
    entrypoint: ["/bin/bash", "-c", "pip install fastapi uvicorn &&  python /main.py"]

Now the spec lives at /v1/docs on your fastAPI server. However, when a client requests the docs using the proxy via localhost:8080/api/v1/docs then the client will be instructed to get the spec at /v1/docs, which resolves to localhost:8080/v1/docs and is a path that doesn't exist.

openapi_url alone won't solve this problem for you, because you can't move the spec outside of the /api/v1/ path on the proxy. You might try something like openapi_url="/api/v1/openapi.json", but then your spec will live at localhost:8080/api/v1/api/v1/openapi.json which is again not what you want. An additional location block is really what you want.

The more interesting question is how to solve the problem for multiple APIs (as posed by the OP). The cleanest way I can think of is adding openapi_url="/<api_name>/<version>/openapi.json" on each fastapi server (so that nginx can differentiate them when the client requests them) and a location of the form

(warning: untested)

location ~ /(?<apiName>[^/]+)/(?<version>[^/]+)/openapi.json {
  map $apiName $apiRoute{
    api    api;
  }
  return 301 /$apiRoute/$version;
  # alternatively rewrite ... if necessary
}

or a single location block for every API as suggested above.

This way, the api doesn't need to know where it actually ends up living on the server (which may be different for dev, stage, production). It is not ideal, because the API still has to know about it's version, and (more importantly) adding a new API means changing server.conf each time. Maybe you ( @wshayes ) or someone else has a better idea?

Was this page helpful?
0 / 5 - 0 ratings