diff --git a/docs/introduction/configuration.md b/docs/introduction/configuration.md index 4ab949f..4742be6 100644 --- a/docs/introduction/configuration.md +++ b/docs/introduction/configuration.md @@ -464,8 +464,8 @@ To bootstrap CORS headers, you must provide `cors_allowed_origins` or `cors_allo Additional params: -- `cors_allowed_methods` -- `cors_allowed_headers` +- `cors_allowed_methods` defaults to `["*"]`, allowing all standard HTTP methods. +- `cors_allowed_headers` defaults to `[]`, allowing only the framework’s built-in CORS headers. - `cors_exposed_headers` - `cors_allowed_credentials` - `cors_max_age` diff --git a/docs/migration/to-2.x.md b/docs/migration/to-2.x.md new file mode 100644 index 0000000..ac2538b --- /dev/null +++ b/docs/migration/to-2.x.md @@ -0,0 +1,14 @@ +# Migration guide for lite-bootstrap 2.x + +## CORS methods on FastAPI and Litestar + +In 2.x, `cors_allowed_methods` defaults to `["*"]`. If you configure allowed +origins and omit the method list, preflight requests can now succeed for all +standard HTTP methods, including POST. + +Set `cors_allowed_methods` explicitly to restrict cross-origin requests to the +methods your application supports, such as `["GET", "POST"]`. + +`cors_allowed_headers` defaults to `[]`. Both frameworks allow their built-in +CORS headers, including `Content-Type`. Add headers such as `Authorization` +explicitly if your application needs them. diff --git a/lite_bootstrap/instruments/cors_instrument.py b/lite_bootstrap/instruments/cors_instrument.py index d97a978..a723c0a 100644 --- a/lite_bootstrap/instruments/cors_instrument.py +++ b/lite_bootstrap/instruments/cors_instrument.py @@ -11,7 +11,7 @@ @dataclasses.dataclass(kw_only=True, frozen=True) class CorsConfig(BaseConfig): cors_allowed_origins: list[str] = dataclasses.field(default_factory=list) - cors_allowed_methods: list[str] = dataclasses.field(default_factory=list) + cors_allowed_methods: list[str] = dataclasses.field(default_factory=lambda: ["*"]) cors_allowed_headers: list[str] = dataclasses.field(default_factory=list) cors_exposed_headers: list[str] = dataclasses.field(default_factory=list) cors_allowed_credentials: bool = False diff --git a/mkdocs.yml b/mkdocs.yml index bf4d835..7531150 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,8 @@ nav: - Installation: introduction/installation.md - Configuration: introduction/configuration.md - Performance: introduction/performance.md + - Migration: + - Upgrading to 2.x: migration/to-2.x.md - Integrations: - Litestar: integrations/litestar.md - FastStream: integrations/faststream.md diff --git a/tests/instruments/test_cors_instrument.py b/tests/instruments/test_cors_instrument.py index 092cc88..2ca22ae 100644 --- a/tests/instruments/test_cors_instrument.py +++ b/tests/instruments/test_cors_instrument.py @@ -31,7 +31,7 @@ def test_cors_instrument_configured_with_both() -> None: def test_cors_instrument_config_defaults() -> None: config = CorsConfig() assert config.cors_allowed_origins == [] - assert config.cors_allowed_methods == [] + assert config.cors_allowed_methods == ["*"] assert config.cors_allowed_headers == [] assert config.cors_exposed_headers == [] assert config.cors_allowed_credentials is False diff --git a/tests/test_fastapi_bootstrap.py b/tests/test_fastapi_bootstrap.py index ef9612a..3cb11af 100644 --- a/tests/test_fastapi_bootstrap.py +++ b/tests/test_fastapi_bootstrap.py @@ -55,6 +55,27 @@ def test_config_rejects_comma_in_opentelemetry_excluded_url( config_class(opentelemetry_excluded_urls=[pattern]) +def test_origins_only_cors_allows_json_post_preflight() -> None: + origin = "https://a.example" + config = FastAPIConfig(cors_allowed_origins=[origin]) + application = FastAPIBootstrapper(bootstrap_config=config).bootstrap() + + with TestClient(application) as client: + response = client.options( + "/x", + headers={ + "Origin": origin, + "Access-Control-Request-Method": "POST", + "Access-Control-Request-Headers": "content-type", + }, + ) + + assert response.is_success + assert response.headers["access-control-allow-origin"] == origin + assert "POST" in response.headers["access-control-allow-methods"].split(", ") + assert "content-type" in response.headers["access-control-allow-headers"].lower().split(", ") + + @pytest.fixture def fastapi_config() -> FastAPIConfig: return FastAPIConfig( diff --git a/tests/test_litestar_bootstrap.py b/tests/test_litestar_bootstrap.py index 5901273..4058755 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -55,6 +55,27 @@ logger = structlog.getLogger(__name__) +def test_origins_only_cors_allows_json_post_preflight() -> None: + origin = "https://a.example" + config = LitestarConfig(cors_allowed_origins=[origin]) + application = LitestarBootstrapper(bootstrap_config=config).bootstrap() + + with TestClient(application) as client: + response = client.options( + "/x", + headers={ + "Origin": origin, + "Access-Control-Request-Method": "POST", + "Access-Control-Request-Headers": "content-type", + }, + ) + + assert response.is_success + assert response.headers["access-control-allow-origin"] == origin + assert "POST" in response.headers["access-control-allow-methods"].split(", ") + assert "content-type" in response.headers["access-control-allow-headers"].lower().split(", ") + + @pytest.fixture def litestar_config() -> LitestarConfig: return LitestarConfig(