Skip to content

fix(backend): parse BACKEND_CORS_ORIGINS correctly instead of bracket-string literal - #27

Merged
vladmesh merged 1 commit into
mainfrom
vladmesh/personal_site-219-1783011997
Jul 2, 2026
Merged

vladmesh merged 1 commit into
mainfrom
vladmesh/personal_site-219-1783011997

Conversation

@vladmesh

@vladmesh vladmesh commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • BACKEND_CORS_ORIGINS was split on commas even when compose supplied a JSON array default (["http://localhost"]), so the real origin became the literal bracketed/quoted string and never matched a browser's Origin header — CORS was effectively broken in every environment.
  • pydantic-settings also pre-decodes list-typed env values as JSON before any field validator runs, so a plain comma-separated value (the format documented in .env.example/README) would have crashed startup outright once it reached that code path.
  • Fixed the validator to accept both a JSON array string and a comma-separated string, opted the field out of pydantic-settings' automatic JSON pre-decode (NoDecode), and raises a clear error for anything else (malformed JSON, non-string array items, empty value).
  • Aligned docker-compose.yml and docker-compose.stand.yml defaults plus .env.stand on the comma-separated format already used in .env.example/README, so there's one format across dev/stand instead of a silent split.
  • docker-compose.prod.yml never set BACKEND_CORS_ORIGINS and is left untouched — adding it would be expanding CORS config, which is out of scope.

Acceptance criteria

  • Parser accepts JSON array and comma-separated string, output is always clean URLs: done, see services/backend/tests/api/test_config.py::TestParseCorsOrigins and TestSettingsFromEnv (the latter exercises the real pydantic-settings env pipeline, not just the validator function).
  • dev/stand compose values consistent with the parser, one format, no silent divergence: done — docker-compose.yml, docker-compose.stand.yml, .env.stand now all use the comma-separated format.
  • Unit test for both formats + garbage value with a clear error: done, same test file (13 tests, includes malformed JSON, non-string JSON array, and empty-string cases).
  • Live check on stand: built and ran the isolated personal_site_stand compose stack with BACKEND_CORS_ORIGINS=http://localhost:4331 from .env.stand. curl -H "Origin: http://localhost:4331" against /api/health and an OPTIONS preflight against /api/v1/profile both returned access-control-allow-origin: http://localhost:4331 (clean, no brackets/quotes); a mismatched Origin: http://evil.example got no CORS header. Confirmed in-container that Settings().BACKEND_CORS_ORIGINS == ['http://localhost:4331'].

Full backend suite (pytest --cov=app, ruff check, ruff format --check, mypy) passes; pre-existing ruff-format findings in 3 unrelated alembic files and profile.py are untouched by this change (verified against the pre-change tree).

…-string literal

pydantic-settings pre-decodes list-typed env values as JSON before the field
validator runs, so a plain comma-separated origin list crashed startup, while
the JSON-array default in compose (["http://localhost"]) slipped through as
the literal bracketed string, matching no real Origin header. Opt the field
out of that pre-decode (NoDecode) and parse JSON array or comma-separated
strings explicitly, rejecting anything else. Align dev/stand compose defaults
on the comma-separated format already documented in .env.example/README.
@vladmesh
vladmesh merged commit 2b8bfb0 into main Jul 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant