When API_DOCS_ENABLED=false, FastAPI registers no routes for /docs, /redoc, or /openapi.json, returning 404 for all three. Default is true for backwards compatibility. Invalid values fall back to true (FR-007). Fix: Remove tests/ and alembic/ from api/.dockerignore so the test Dockerfile (which uses COPY . .) includes the test suite; Dockerfile.prod is unaffected as it only copies app/ explicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.0 KiB
Research: API Documentation Visibility Gate
Decision 1: Env var name
Decision: API_DOCS_ENABLED (boolean, default true)
Rationale: Consistent with the existing API_BASE_URL naming convention in the project. The positive-phrasing default (true = enabled) preserves backwards compatibility — existing deployments that don't set the variable get the same behaviour as today.
Alternatives considered: HIDE_API_DOCS=false (negative phrasing) — inverted booleans are error-prone and confusing in .env files; DOCS_ENABLED — too generic, could collide with other tools in a multi-service env file.
Decision 2: FastAPI docs suppression mechanism
Decision: Pass docs_url=None, redoc_url=None, openapi_url=None to the FastAPI() constructor when the flag is disabled.
Rationale: This is the official FastAPI-supported mechanism. Setting these to None causes FastAPI to register no routes for those paths — requests to them fall through to the default 404 handler. The internal OpenAPI schema is still generated in memory (for request validation), but no HTTP route exposes it.
Alternatives considered: Route-level middleware that intercepts and returns 404 — more complex, not the canonical approach; removing routers at runtime — impossible, routers are registered at import time.
Decision 3: Settings read at module level
Decision: Read get_settings() once at module import time in main.py to configure the FastAPI() constructor.
Rationale: FastAPI() is instantiated at module level; the docs URL parameters must be known at that point. get_settings() is already @lru_cache so calling it at module level is cheap and consistent with calling it again inside lifespan. Tests that need to change the flag must reload the module or override get_settings.
Alternatives considered: Lazy initialisation of app inside a factory function — would require restructuring main.py and all imports; not worth the complexity for this change.
Decision 4: Graceful fallback for invalid flag values (FR-007)
Decision: Add a @field_validator('api_docs_enabled', mode='before') in Settings that wraps Pydantic's bool coercion in a try/except and returns True on any ValueError.
Rationale: Pydantic v2 raises ValidationError for unrecognised boolean strings (e.g., API_DOCS_ENABLED=maybe). FR-007 requires the app to start rather than fail. The validator intercepts the invalid value before Pydantic's own coercion and returns the safe default.
Alternatives considered: Using Optional[bool] = True without a validator — Pydantic would still raise on invalid input; using str field with manual parsing — duplicates Pydantic's boolean parsing logic unnecessarily.
Decision 5: Integration test approach
Decision: Test both enabled and disabled states by overriding get_settings in integration tests using app.dependency_overrides, or by constructing a local FastAPI app instance with the appropriate docs_url/redoc_url/openapi_url values.
Rationale: The app in app.main is created at import time. Since the unit tests already use monkeypatch + importlib.reload for config changes, the integration tests for docs visibility can follow the same pattern — reload app.main with the env var set before importing app. Alternatively, test the URL routing behaviour directly by constructing a minimal test app.
Alternatives considered: Patching app.docs_url after import — FastAPI does not re-register routes when these attributes are changed post-construction; no effect on routing.
Decision 6: Production documentation
Decision: Update .env.example to include API_DOCS_ENABLED=true with a comment recommending false for production. No changes to api/Dockerfile.prod (env vars are supplied by the deployment environment, not the image).
Rationale: The Dockerfile intentionally contains no runtime secrets or config. The .env.example is the canonical documentation for operators. A comment is sufficient; the production Dockerfile.prod already has no docs-related config.