Skip to content

fix(api): boot when only services/api is deployed - #65

Merged
EduPav merged 1 commit into
mainfrom
fix/api-boots-from-service-root
Aug 13, 2026
Merged

fix(api): boot when only services/api is deployed#65
EduPav merged 1 commit into
mainfrom
fix/api-boots-from-service-root

Conversation

@EduPav

@EduPav EduPav commented Aug 13, 2026

Copy link
Copy Markdown
Member

Stacked on #64 — review that one first. Base retargets to main automatically when #64 merges.

What

The API crashed at import time, before serving a single request, whenever the deployed
tree is the service directory rather than the whole repository.

_counter_path() reached the repo root with Path(__file__).resolve().parents[4], which
assumes six levels: repo/services/api/app/repo/counter.py. Deploy only services/api
a platform with the service root directory set to it, or a Docker build that copies just
that directory — and the tree is /app/app/repo/counter.py: four levels. parents[4] then
indexes past the filesystem root and raises IndexError, which propagates through
app.repo.__init__app.service.filesapp.runtime.filesmain, so uvicorn dies
inside load_app().

The failure signature is nasty: the build succeeds, the image is pushed, and then the
health check simply never passes — 1/1 replicas never became healthy. Nothing in the build
log hints at a Python error, because the crash happens in the runtime container.

Both call sites shared the assumption:

  • app/repo/counter.py now anchors at the repo root when those ancestors exist, and at
    this service's own root otherwise. The reason for preferring the repo root is unchanged —
    uvicorn --reload watches services/api/, so runtime state written there triggers reload
    noise — and it simply does not apply where there is no reloader.
  • main.py resolved the repository-root dotenv file three levels up, landing on a
    nonexistent path at the filesystem root in a service-only tree. That one failed silently
    rather than crashing (configuration comes from real environment variables there, and
    load_dotenv on a missing file is a no-op), but the comment claimed the path "resolves
    correctly regardless of where uvicorn is invoked from", which was not true.

Why it was never caught

The suite monkeypatches download_count_file to an absolute tmp path, so the relative
branch — the only one that anchors — was never exercised. And the deployment target that
exposes it is new: on Vercel the whole repository is uploaded and the entrypoint is
services/api/index.py, so all six levels exist and parents[4] resolves fine.

tests/test_counter_path.py now pins the full-checkout layout, the service-only layout, and
the absolute-path bypass, so neither anchor can regress to the other.

Checks run by Claude

  • pnpm verify:api — green: ruff, 187 passed (184 + the 3 added here), and all four
    structure tests including the file-size invariant
  • reproduced the original crash on a real two-service deployment with the API's service root
    set to services/api: build SUCCESS, then health check failing six attempts over its
    100s window, with the IndexError traceback in the runtime logs
  • grepped every __file__ / parents[N] anchor under services/api; the only other two are
    in scripts/ (export_openapi.py, setup_b2_cors.py), which are developer tools run from
    a checkout and are correct as they stand

Manual checks

None outstanding — the fix is verified by the redeployed service booting and answering
/health, which is covered in the deployment evidence rather than left to a reviewer.

The API crashed at import time — before serving a single request — whenever the
deployed tree is the service directory rather than the whole repository.

`_counter_path()` reached the repo root with
`Path(__file__).resolve().parents[4]`, which assumes six levels
(repo/services/api/app/repo/counter.py). Deploy only `services/api` — Railway
with the service root directory set to it, or a Docker build that copies just
this directory — and the tree is `/app/app/repo/counter.py`: four levels, so
`parents[4]` indexes past the filesystem root and raises `IndexError`. That
propagates through `app.repo.__init__` -> `app.service.files` ->
`app.runtime.files` -> `main`, so uvicorn dies during `load_app()`. Observed on
Railway as a successful build followed by a health check that never passes.

Both call sites made the same assumption:

- `app/repo/counter.py` now anchors at the repo root when those ancestors exist
  and at this service's own root otherwise. The reason for preferring the repo
  root is unchanged — `uvicorn --reload` watches services/api/, so runtime state
  written there causes reload noise — and it simply does not apply to a
  deployment with no reloader.
- `main.py` resolved the repo-root `.env` three levels up, landing on `/.env` in
  a service-only tree. That failed silently rather than crashing (configuration
  comes from real environment variables there, and `load_dotenv` on a missing
  file is a no-op), but the comment claimed the path resolved correctly from
  anywhere, which was not true.

`tests/test_counter_path.py` pins both layouts and the absolute-path bypass, so
neither anchor can regress. Nothing existing covered this: the suite
monkeypatches `download_count_file` to an absolute tmp path and never exercises
the relative branch.
@EduPav
EduPav force-pushed the fix/api-boots-from-service-root branch from a551d7b to 7f8b11f Compare August 13, 2026 20:35
@EduPav
EduPav changed the base branch from feat/railway-default-config-paths to main August 13, 2026 20:35
@EduPav
EduPav marked this pull request as ready for review August 13, 2026 20:37
@EduPav
EduPav merged commit 6fb1e1a into main Aug 13, 2026
7 checks passed
@EduPav
EduPav deleted the fix/api-boots-from-service-root branch August 13, 2026 20:51
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