Skip to content

Commit 04ffe3a

Browse files
Document and test CORS preflights for missing-route error responses
1 parent ad4f355 commit 04ffe3a

3 files changed

Lines changed: 69 additions & 0 deletions

File tree

‎docs/examples/decorator.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,24 @@ Flask-CORS can be used as a decorator on views, which makes it easy to isolate i
55
```py
66
--8<-- "examples/view_based_example.py"
77
```
8+
# Reading errors after a preflight request
9+
10+
For an unknown URL, Flask normally returns 404 to both `OPTIONS` and the actual
11+
request. A browser requires a successful preflight response before sending the
12+
actual request, so it may report a CORS failure without exposing the 404 body to
13+
JavaScript. Adding CORS headers to the 404 preflight alone does not make it succeed.
14+
15+
If your application needs clients to read these errors, apply `cross_origin` to
16+
the application's 404 handler. Keep `automatic_options=True` (the default): the
17+
decorator answers `OPTIONS` with 200, while the handler still returns 404 for the
18+
actual request. Place `@app.errorhandler` above `@cross_origin` so Flask registers
19+
the decorated handler. Configure the origin, methods, and headers for your API:
20+
21+
```py
22+
--8<-- "examples/error_handler_example.py"
23+
```
24+
25+
For example, a `GET /missing` with an `X-Request-ID` header from
26+
`https://client.example` can now complete preflight and expose the JSON 404 response.
27+
This is an application-level handler for all missing routes; choose its CORS
28+
policy accordingly. A successful preflight does not make the requested URL exist.

‎examples/error_handler_example.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Allow a browser to read a missing route's JSON error after preflight."""
2+
3+
from flask import Flask
4+
5+
from flask_cors import cross_origin
6+
7+
app = Flask(__name__)
8+
9+
10+
@app.errorhandler(404)
11+
@cross_origin(origins=["https://client.example"], methods=["GET"], allow_headers=["X-Request-ID"])
12+
def not_found(error):
13+
return {"error": "Not found"}, 404
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from examples.error_handler_example import app
2+
3+
4+
def test_missing_route_preflight_and_error_response():
5+
client = app.test_client()
6+
response = client.options(
7+
"/missing",
8+
headers={
9+
"Origin": "https://client.example",
10+
"Access-Control-Request-Method": "GET",
11+
"Access-Control-Request-Headers": "X-Request-ID",
12+
},
13+
)
14+
assert response.status_code == 200
15+
assert response.headers["Access-Control-Allow-Origin"] == "https://client.example"
16+
assert response.headers["Access-Control-Allow-Methods"] == "GET"
17+
assert response.headers["Access-Control-Allow-Headers"] == "X-Request-ID"
18+
19+
response = client.get("/missing", headers={"Origin": "https://client.example", "X-Request-ID": "123"})
20+
assert response.status_code == 404
21+
assert response.json == {"error": "Not found"}
22+
assert response.headers["Access-Control-Allow-Origin"] == "https://client.example"
23+
assert client.get("/missing").status_code == 404
24+
25+
26+
def test_error_handler_rejects_other_origins_and_methods():
27+
client = app.test_client()
28+
for method in (client.get, client.options):
29+
response = method("/missing", headers={"Origin": "https://other.example"})
30+
assert "Access-Control-Allow-Origin" not in response.headers
31+
response = client.options(
32+
"/missing",
33+
headers={"Origin": "https://client.example", "Access-Control-Request-Method": "DELETE"},
34+
)
35+
assert "Access-Control-Allow-Methods" not in response.headers

0 commit comments

Comments
 (0)