Closed oeway closed 2 years ago
Doesn't this contradict the very idea of the CORS? it seems to me that it would be better to validate the Origin header on the backend against the list of allowed sources
Doesn't this contradict the very idea of the CORS? it seems to me that it would be better to validate the Origin header on the backend against the list of allowed sources
Sorry that I don't get what you mean. Do you mean I should not use the cors middleware? In this case we only want the cors header to be sent correctly for all the resources. When I say differences cases, I mean the same origin header in upper and lower cases.
I did some more experiments and found out the issue is actually coming from calling "MutableHeaders.update()" used in the CORSMiddleware
.
See the example below:
@app.middleware("http")
async def add_cors_header(request: Request, call_next):
response = await call_next(request)
print('==========before====>headers', response.headers)
headers = {"Access-Control-Allow-Credentials": "true"}
response.headers.update(headers)
print('==========after====>headers', response.headers)
return response
And this is what I see from the console:
==========before====>headers MutableHeaders({'Content-Type': 'text/plain; charset=UTF-8', 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true'})
==========after====>headers MutableHeaders({'Content-Type': 'text/plain; charset=UTF-8', 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true', 'access-control-allow-credentials': 'true'})
As you can see, for some reason, when we update the headers, it results in both versions: 'Access-Control-Allow-Credentials' and 'access-control-allow-credentials'.
Merged in #235. Can be closed.
Hi @aminalaee thanks for looking into it. However it seems #235 is about test client, I don't get why that can fix the issue mentioned here, could you please clarify?
@oeway Sorry, closed the wrong issue. I'll re-open this.
I do not think that this is a bug in starlette, but a problem in engineio. It appears that engineio is adding headers without checking for duplicates.
In engineio/server.py, one finds the following at line 460:
cors_headers = self._cors_headers(environ)
start_response(r['status'], r['headers'] + cors_headers)
I will likely take this up with engineio, but the practical fix, at least for me, is to disable CORS handling in socketio by setting cors_allowed_origins=[] when creating the engineio server object. For fastapi-socketio:
socket_manager = SocketManager(app=app, cors_allowed_origins=[])
I should probably also complain to unicorn for sending multiple response headers with the same name; servers aren't supposed to do this except for Set-Cookie and certain other headers like Vary.
I should probably also complain to unicorn for sending multiple response headers with the same name; servers aren't supposed to do this except for Set-Cookie and certain other headers like Vary.
https://asgi.readthedocs.io/en/latest/specs/www.html#http
The ASGI design decision is to transport both request and response headers as lists of 2-element [name, value] lists and preserve headers exactly as they were provided.
This is an unfortunate design decision that appears to be based on a misreading of RFC7230. ASGI spec:
"Multiple header fields with the same name are complex in HTTP. RFC 7230 states that for any header field that can appear multiple times, it is exactly equivalent to sending that header field only once with all the values joined by commas.
However, RFC 7230 and RFC 6265 make it clear that this rule does not apply to the various headers used by HTTP cookies (Cookie and Set-Cookie). The Cookie header must only be sent once by a user-agent, but the Set-Cookie header may appear repeatedly and cannot be joined by commas. The ASGI design decision is to transport both request and response headers as lists of 2-element [name, value] lists and preserve headers exactly as they were provided."
RFC 7230:
" A sender MUST NOT generate multiple header fields with the same field name in a message unless either the entire field value for that header field is defined as a comma-separated list [i.e., #(values)] or the header field is a well-known exception (as noted below)."
Not all header fields are defined as comma-separated lists.
I meant that uvicorn follows ASGI specs, so you should probably "complain" on asgiref.
Ah. Thanks for the link.
@declaresub I understand that it's more relevant to fix from the spec and starlette implemented it correctly. However, for practical reasons, do you think we can have a fix in the CORSMiddleware? Instead of blindly add the CORS headers, we should check whether the same header (in different capitalization) exists already. What do you think?
For this particular issue, the fix needed is to use exactly one of starlette or engine.io to handle CORS, as noted earlier. I haven't gone through cors.py as carefully as I could, but I didn't see any place where headers were being appended. Perhaps it would be useful to make sure that it overwrites any existing headers that it sets.
The real solution to sloppy middleware is, of course, more middleware. I have it in mind to write some header middleware that checks for multiple headers with the same name, and does something when such are found. I don't believe this can be done reliably in starlette, because middleware can always be added outside the control of starlette.
For this particular issue, the fix needed is to use exactly one of starlette or engine.io to handle CORS, as noted earlier.
If that's the case, I don't think we have an issue on Starlette.
I don't think it makes sense for the CORSMiddleware
to verify if the headers Access-Control-Allow-Origin
are present, as the middleware itself should be in charge of adding them.
Given the statement above:
For this particular issue, the fix needed is to use exactly one of starlette or engine.io to handle CORS, as noted earlier.
I'm closing the issue. If you think this is not solved, or there's a different point of view that was not presented, feel free to express yourself here, in a new issue, or reach out to us on our Gitter channel.
I mailed you, so that i don't have to reopen this, but I am having a weird behavior here,
origins = [
"*"
]
# Temporary fix for CORS issue
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
everytime i add a single CORS middleware origin, I grepped, its the only call, it adds two and yields the known bug.
Any suggestion?
Please explain as if I was retarded. (Sorry, spent a night.) =)
I mean, you are using python-socketio, right?
I mean, you are using python-socketio, right?
FastAPI with uvicorn
Can you share a minimal reproducible example?
https://github.com/lyrasadie/lyra-api/blob/main/api/__init__.py
When not commented out, 2 CORS headers. When, none.
Can you share a minimal reproducible example?
https://github.com/lyrasadie/lyra-api/blob/main/api/__init__.py
When not commented out, 2 CORS headers. When, none. Aware of the missing [ ] in comment
@zdanl did you ever figure this out?
i stuck into the same problem. my app uses both FastAPI and python-socketio.
origins = [
# "http://mydomain:5173",
]
is there any workaround for this?
Checklist
master
.Describe the bug
When using starlette (through FastAPI) CORSMiddleware with python-socketio (another ASGI app mounted via app.mount()), I got multiple values for the access-control-allow-origin headers. This is because both python socketio and the CORSMiddleware will inject CORS headers in different cases. In the response I see two duplicated headers:
And the browser complain it as a CORS error.
To reproduce
Expected behavior
The CORSMiddleware should check existing headers in different cases and update them only if the headers in different cases not exists.
Actual behavior
The CORSMiddleware add the cors haders and result in multiple duplicated values.
Environment
Additional context