From 3ddcc02e2ff796b35731c20bada46c8707f4c505 Mon Sep 17 00:00:00 2001 From: Mikan <72257910+Mikan-DS@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:54:41 +0300 Subject: [PATCH] fix --- backend/app/config.py | 72 +++++++++++++++++++++++++++---------------- backend/app/main.py | 2 +- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 03d58cf..b46a140 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -4,10 +4,43 @@ from __future__ import annotations import os import secrets from functools import lru_cache -from typing import Annotated, List +from typing import List -from pydantic import Field, field_validator -from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +def _parse_cors_origins(raw: str) -> List[str]: + """Parse a CORS_ORIGINS env value into a list of origin strings. + + Accepts (in priority order): + - JSON array string: '["http://a","http://b"]' + - comma-separated: "http://a,http://b" + - single value: "http://a" + Strips whitespace and drops empties. Returns the default list if input + is empty/blank. + + This helper exists because pydantic-settings' EnvSettingsSource treats + `List[str]` as a "complex" type and tries to JSON-decode the raw env + value BEFORE any field validator runs — so a plain comma-separated + string from docker-compose crashes Settings() at import time on + older pydantic-settings versions. Storing the field as `str` sidesteps + that entirely; this helper parses it on demand. + """ + if not raw: + return ["http://localhost:5173"] + v = raw.strip() + if not v: + return ["http://localhost:5173"] + if v.startswith("["): + import json + try: + parsed = json.loads(v) + if isinstance(parsed, list): + return [str(o).strip() for o in parsed if str(o).strip()] + except Exception: + pass # fall through to comma-split + return [o.strip() for o in v.split(",") if o.strip()] class Settings(BaseSettings): @@ -32,30 +65,17 @@ class Settings(BaseSettings): admin_setup_token: str = "" # CORS - # `NoDecode` tells pydantic-settings NOT to JSON-parse the env value, so a - # plain comma-separated string like "http://localhost:5173,http://localhost:8080" - # reaches our `@field_validator` intact, which then splits it into a list. - cors_origins: Annotated[List[str], NoDecode] = Field( - default_factory=lambda: ["http://localhost:5173"] - ) + # Stored as a raw string (NOT List[str]) so pydantic-settings' env source + # treats it as a simple scalar and never attempts JSON-decoding. The + # parsed list is exposed via the `cors_origins_list` property below. + # Accepts either a comma-separated string ("http://a,http://b") or a + # JSON-array string ('["http://a","http://b"]'). + cors_origins: str = "http://localhost:5173" - @field_validator("cors_origins", mode="before") - @classmethod - def _split_origins(cls, v): - if isinstance(v, str): - # Allow both JSON arrays (e.g. '["http://a","http://b"]') and plain - # comma-separated strings (e.g. "http://a,http://b") from env vars. - v = v.strip() - if v.startswith("["): - import json - try: - return [o.strip() for o in json.loads(v) if o.strip()] - except Exception: - pass - return [o.strip() for o in v.split(",") if o.strip()] - if isinstance(v, (list, tuple)): - return [str(o).strip() for o in v if str(o).strip()] - return v + @property + def cors_origins_list(self) -> List[str]: + """Parsed list of allowed CORS origins (see `_parse_cors_origins`).""" + return _parse_cors_origins(self.cors_origins) # Logging log_level: str = "INFO" diff --git a/backend/app/main.py b/backend/app/main.py index 3303350..d424591 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -50,7 +50,7 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=settings.cors_origins, + allow_origins=settings.cors_origins_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"],