简体   繁体   中英

How to configure traefik to handle CORS preflight requests?

I have a very simple FastAPI-Traefik-Docker Setup which you can find here: https://github.com/mshajarrazip/fastapi-traefik-docker-cors

Just do docker-compose up -d to run the FastAPI and traefik services.

TLS/SSL is not configured in this setup.

The app has two endpoints GET "/hello" and POST "/add" which does nothing but return json reponses:

In app/main.py

import logging

from app import routes
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

def create_application() -> FastAPI:
    app = FastAPI(title="✨ NAC ML API ✨")

    app.include_router(routes.hello.router)

    # add CORS
    # origins = ["*"]
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"], # Allows all origins
        allow_credentials=True,
        allow_methods=["*"], # Allows all methods
        allow_headers=["*"], # Allows all headers
        )

    return app


app = create_application()

In app/routes/...

from fastapi import APIRouter

router = APIRouter()

@router.get("/hello")
def hello():
    return {
        "message": "Hello!"
    }

@router.post("/add")
def add_to_db():
    return {
        "id": 1, 
        "name": "Salmah",
        "age": 29
    }

Calling these endpoints via curl and Postman works:

curl -X 'GET' http://172.16.239.132:81/hello -H 'Host: basic.api'
curl -X 'POST' http://172.16.239.132:81/add -H 'Host: basic.api'

But not from a simple javascript fetch in a simple html file running on the Chrome browser (from my local computer to the remote server where traefik is running):

<html>
    <Button id="btnAddData" onclick="AddData();"> Add Data </Button>
</html>

<script>
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Host", "basic.api");

var requestOptions = {
  method: 'POST',
  headers: myHeaders,
  redirect: 'follow'
};

function AddData(){
  console.log("click");

  fetch("http://172.16.239.132:81/add", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));
}
</script>

Response:

Access to fetch at 'http://172.16.239.132:81/add' from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

The traefik logs via docker-compose logs -f reverse-proxy :

reverse-proxy_1  | 10.32.26.13 - - [18/Dec/2022:03:34:45 +0000] "OPTIONS /add HTTP/1.1"  404 19 "-" "-" 9 "-" "-" 0ms

I figured the issue may be because I was running fetch from the local file system, so instead I emulate the OPTIONS request using curl (and this is requesting for GET):

curl -i -X OPTIONS \
    -H 'Access-Control-Request-Method: GET' \
    -H 'Access-Control-Request-Headers: Content-Type' \
    -H 'Host: basic.api' \
    "http://172.16.239.132:81/hello"

I get a 405 Method not allowed error:

HTTP/1.1 405 Method Not Allowed
Allow: GET
Content-Length: 31
Content-Type: application/json
Date: Sun, 18 Dec 2022 03:39:40 GMT
Server: uvicorn

{"detail":"Method Not Allowed"}

Same goes when I set 'Access-Control-Request-Method: OPTIONS' .

This is my traefik setup in docker-compose.yml:

services:
  reverse-proxy:
    image: traefik:v2.9
    ports:
      - ${API_PORT}:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik.yml:/etc/traefik/traefik.yml
    labels:
      - "traefik.http.middlewares.cors.headers.accesscontrolallowmethods=*"
      - "traefik.http.middlewares.cors.headers.accesscontrolalloworiginlist=*"
      - "traefik.http.middlewares.cors.headers.accesscontrolmaxage=100"
      - "traefik.http.middlewares.cors.headers.addvaryheader=true"

I followed the recommendation here from the official Traefik docs on CORS headers , which obviously isn't all there is I need to know.

I'm not a CORS.networking expert so that adds to my current set of difficulties:( I don't know how to solve this issue. I've been at it for five days with various combination of solutions. At first I suspected it was FastAPI's CORSMiddleware module that was buggy - but the CORSMiddleware set up in the FastAPI code is working as I tested it without traefik and it worked fine.

I'm sure it's a common issue among newbies to traefik but everywhere I googled, I couldn't find a straightforward answer (or maybe the answer is already there I just don't understand it). Plus, all their setup are not as simple as my current setup (they have TLS/SSL configured and everything).

I don't think the issue is FastAPI so much as it is traefik itself. When calling the endpoints via fetch, I don't think the requests even reaches the FastAPI app. So any solutions should probably apply at the traefik reverse proxy and hence, should be applicable to any other deployments regardless if it's FastAPI or not.

Please help:( I'm sure it will help a lot of beginners too.

As it turns out, if there is an error somewhere in traefik's configuration eg the labels on your service is not set correctly, you will always get the CORS error (bad config = no response to preflight request = CORS error). For FastAPI, the in-built CORSMiddleware should work fine even without configuring CORS at traefik level.

My mistake was not realising the error is setting up the labels for my FastAPI service.

Somehow, changing from this:

api:
  ...
  labels: # new
    - "traefik.enable=true"
    - "traefik.http.routers.to_api.rule=Host(`basic.api`) && PathPrefix(`/basic/`)"

to this ( && -> || ):

api:
  ...
  labels: # new
    - "traefik.enable=true"
    - "traefik.http.routers.to_api.rule=Host(`basic.api`) || PathPrefix(`/basic/`)"

Fixes the issue - my requests are now routed correctly to FastAPI for its CORS middleware to handle the preflights. But still, || is not what I really intended - either I have a misconception of the rules or it is a bug? I have no answer at the moment.

To enable CORS for your backend service in the docker-compose.yml file, you can add the following labels to the backend service:

  # Enable CORS headers
  - "traefik.http.middlewares.cors.headers.accesscontrolallowmethods=*"
  - "traefik.http.middlewares.cors.headers.accesscontrolalloworiginlist=*"
  - "traefik.http.middlewares.cors.headers.accesscontrolmaxage=100"
  - "traefik.http.middlewares.cors.headers.addvaryheader=true"

These labels will configure the CORS middleware to allow any HTTP method, any origin, and a maximum age of 100 seconds for the CORS headers. The addvaryheader flag will also add a Vary header to the response to indicate that the response may vary based on the Origin header.

Once you have added these labels to the backend service, you can apply the CORS middleware to your routers by adding the middleware to the router's middlewares label. For example:

  # Use the CORS middleware for the app-https router
  - traefik.http.routers.app-https.middlewares=cors

This will apply the CORS middleware to the app-https router, allowing the backend service to accept CORS requests from the specified origins. In my case it was https. However; I think you should add this for http

  - traefik.http.routers.my-router.middlewares=cors

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM