Skip to content

docs: Explain FastAPI double initialization with Uvicorn Closes #106 (documentation) - #130

Merged
suppagoddo merged 4 commits into
masterfrom
docs/106-fastapi-init
Oct 21, 2026
Merged

suppagoddo merged 4 commits into
masterfrom
docs/106-fastapi-init

Conversation

@suppagoddo

@suppagoddo suppagoddo commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Final status (head fd1f35f, rebased on master 0cf565b)

Root cause of #106.

  • init_fastapi() may run only once per process. That guard is intentional and unchanged.
  • The report's app/main.py initialises json-logging and then, under if __name__ == "__main__":, calls uvicorn.run("app.main:app").
  • The module body therefore runs twice in one process: first as __main__, then again as app.main when uvicorn imports the string. The second init_fastapi() raises RuntimeError: Can not call init more than once.

Two safe startup patterns (example/fastapi_startup/):

  1. Import string (recommended): python -m uvicorn app.main:app. uvicorn imports app.main once. Required for --reload and --workers.
  2. App object from a separate launcher (run.py): uvicorn.run(app, ...). Suits a simple single-process start.

This round:

  • README verify command fixed. The example README says to run commands from example/fastapi_startup, but gave tests/test_fastapi_startup.py, which finds no tests there. It now gives ../../tests/test_fastapi_startup.py (6 passed when run from that directory) and mentions the root-relative form.

  • Real uvicorn config path. The import-string test now runs uvicorn.Config("app.main:app").load(), which is what the CLI does, instead of import_from_string(). It runs in a fresh subprocess with socket.bind made to fail, and checks that:

    • the loaded app (unwrapped from uvicorn's middleware) is app.main.app;
    • init_fastapi() ran once;
    • no socket was bound.

    A mutation where app/main.py calls init_fastapi() twice and suppresses the error fails it with "init_fastapi() ran 2 times". Checked with uvicorn 0.39 (Python 3.9) and 0.54 (Python 3.12/3.13).

  • Reload and multiple workers documented.

    • uvicorn exits when given an app object with reload or workers > 1; confirmed in the uvicorn 0.39 and 0.54 source.
    • Each worker process initialises once, which is expected.
    • Only two initialisations in one process are an error.
  • Kept unchanged:

    With the guard disabled as a mutation, both negative tests fail.

  • Manual check, not a committed test: a real python -m uvicorn app.main:app server answered with the supplied X-Correlation-ID after one initialisation. With --workers 2 it initialised once per worker, 2 in total, with no error.

  • Rebase: onto master 0cf565b, which includes fix: prevent fabricated correlation IDs outside active requests #127, fix: safely handle correlation ID lookup in non-web mode #128, feat: support configurable exclusion of JSON log fields #129 and ci: add Python 3.14 unit and framework smoke-test coverage #133. Only CHANGELOG.md conflicted; all master entries are kept and the Initialisation error: "Can not call init more than once" [FastAPI] #106 entry is added. The diff against master is docs, example, tests and changelog only. No json_logging/ or tests/test_fastapi.py changes.

Results:

  • Locally: 6/6 in tests/test_fastapi_startup.py; 94/94 full suite on Python 3.9–3.13, in forward and reversed order; each startup test passes alone and after the other FastAPI and field-exclusion suites. Blocking flake8 0.
  • GitHub Actions on fd1f35f: 15/15 (unit tests on Python 3.9–3.14; Flask/Quart/FastAPI/Sanic smoke tests on 3.9 and 3.14; Flake8). Run: https://github.com/bobbui/json-logging-python/actions/runs/39519610612

Not covered by committed tests: --reload (the file-watcher process) and real port binding. These are covered only by the manual check and the uvicorn source.


PR 4 — docs: clarify FastAPI initialisation and Uvicorn startup (Fixes #106)

Branch docs/106-fastapi-init, commit 178a295.

Problem. The reported RuntimeError: Can not call init more than once happens when a module started as a script also
passes an import string to uvicorn, so the module body runs twice.

Reproduced on master by running the reporter's pattern: the error is raised at the second init_fastapi().

Change (docs and example only). example/fastapi_startup/ with app/main.py and a README listing the two working
patterns (python -m uvicorn app.main:app, and passing the app object to uvicorn.run) and the failing pattern. A FAQ
entry points to the example. No library code changes: the protection against double initialisation is kept.

Tests. tests/test_fastapi_startup.py (4 tests): both working patterns import once; the reported pattern still
fails with the same error; a second init in one process still raises. All 4 pass on master, as they are guards.

Local results. Full unit suite on Python 3.9 and 3.13: 39 passed each (35 + 4). Blocking flake8: 0. Not run on
3.10–3.12 in this session.

Not verified. A real uvicorn server was not bound, because the sandbox blocks binding. The working patterns were
checked through uvicorn.importer.import_from_string and by importing the app object.


Review update (round 2)

  • Head 44a0201: the duplicate-initialisation test now raises an explicit AssertionError if the second
    initialisation succeeds, instead of failing only on a missing marker. Verified: with the guard intact, 4/4 pass; with
    the guard removed in a copy, the test fails with the explicit message.
  • Verified: 39/39 on Python 3.9 and 3.13; blocking flake8 clean. The documented startup patterns are unchanged, and the
    core initialisation guard is not modified.

Review update (round 3, head b03cbe8)

The app-object pattern was not actually exercised. Confirmed and fixed.

  • test_app_object_pattern_initialises_once_without_serving only imported app.main. It never ran the documented uvicorn.run(app) launcher, so it could not show that this pattern initialises once.
  • Added example/fastapi_startup/run.py, the launcher the README already showed, and pointed the README at it.
  • New test test_app_object_launcher_passes_the_app_to_uvicorn_and_initialises_once, in a subprocess:
    • wraps json_logging.init_fastapi with a call counter;
    • replaces uvicorn.run with a recorder, so no port is bound and no server starts;
    • runs run.py with runpy.run_path(..., run_name="__main__").
  • It asserts that uvicorn.run() is called once with the app object from sys.modules["app.main"], not a string, and that init_fastapi() ran exactly once.
  • Negative controls (test_launcher_check_detects_a_launcher_that_reimports_the_app): the same check fails for a launcher that reloads app.main (it raises the double-init RuntimeError) and for one that passes "app.main:app" (it trips the assertion).
  • The reported-pattern test and the duplicate-init guard test are unchanged. No library code changed.

Results: 41/41 on Python 3.9–3.13; blocking flake8 0; GitHub Actions 10/10 on b03cbe8.

Not verified: a real uvicorn server binding a port; the sandbox here does not allow it.

Summary by CodeRabbit

  • Documentation

    • Added FAQ and changelog guidance for avoiding duplicate FastAPI logging initialization.
    • Documented recommended Uvicorn startup patterns, reload and worker behavior, and common failure scenarios.
  • New Examples

    • Added a runnable FastAPI startup example with request correlation IDs and JSON logging.
    • Included a separate launcher demonstrating app-object startup.
  • Tests

    • Added coverage for supported startup methods and expected errors from duplicate initialization.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4aa10a99-547e-4c10-9c02-5941a623247c
📥 Commits

Reviewing files that changed from the base of the PR and between 0cf565b and fd1f35f.

📒 Files selected for processing: 7
  • CHANGELOG.md
  • README.md
  • example/fastapi_startup/README.md
  • example/fastapi_startup/app/__init__.py
  • example/fastapi_startup/app/main.py
  • example/fastapi_startup/run.py
  • tests/test_fastapi_startup.py

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

📝 Walkthrough

The pull request adds a FastAPI startup example, documents Uvicorn launch patterns that avoid duplicate initialization, and tests supported startup flows and the resulting RuntimeError for repeated initialization.

Changes: FastAPI startup

Layer / File(s) Summary
App initialization and launcher
example/fastapi_startup/app/main.py, example/fastapi_startup/run.py
The app initializes JSON request instrumentation, configures an INFO-level logger, and exposes a root endpoint that returns the correlation ID. The launcher passes the imported app object to Uvicorn.
Startup guidance
example/fastapi_startup/README.md, README.md, CHANGELOG.md
The documentation describes import-string and app-object startup patterns, reload and worker behavior, and the duplicate-initialization error.
Startup and initialization tests
tests/test_fastapi_startup.py
Isolated-process tests check the supported launch patterns, negative controls, and repeated-initialization errors.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Other · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Uvicorn
  participant AppMain as app.main
  participant JsonLogging as json_logging
  Uvicorn->>AppMain: Import app.main:app
  AppMain->>JsonLogging: Call init_fastapi once
  AppMain-->>Uvicorn: Provide FastAPI app
Loading
Merge Risk: ⚪ Minimal · up to fd1f3

The documented startup patterns and duplicate-initialization behavior present no identified merge-blocking risk.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 3 files. (3 skipped: 3… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check Passed Issue #106 asks for an explanation and advice for the duplicate json_logging.init_fastapi() call. The PR documents that script execution plus the "app.main:app" import string executes the module t…
Out of Scope Changes check Passed The reported changes stay within issue #106. The FAQ, changelog, example application, launcher, and tests support the documented FastAPI and Uvicorn initialization behavior. The tests avoid socket bin…
Title check Passed The title clearly identifies the main change: documenting FastAPI double initialization with Uvicorn. The issue reference and documentation scope are relevant, although the parenthetical is redundant.
Full details: Linked Issues check

Explanation

Issue #106 asks for an explanation and advice for the duplicate json_logging.init_fastapi() call. The PR documents that script execution plus the "app.main:app" import string executes the module twice. It documents python -m uvicorn app.main:app and an app-object uvicorn.run launcher. example/fastapi_startup/ demonstrates these patterns. tests/test_fastapi_startup.py covers both working patterns, the reported failing pattern, and repeated initialization. The existing initialization guard remains unchanged.

Full details: Out of Scope Changes check

Explanation

The reported changes stay within issue #106. The FAQ, changelog, example application, launcher, and tests support the documented FastAPI and Uvicorn initialization behavior. The tests avoid socket binding because they verify configuration and launcher behavior. No unrelated library behavior change is reported.

Full details: Docstring Coverage

Explanation

Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 3 files. (3 skipped: 3 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

json_logging.init_fastapi() can run only once per process. The reported
RuntimeError ("Can not call init more than once") occurs when a module is
started as a script and uvicorn also imports it through an import string,
so the module body runs twice.

Adds example/fastapi_startup, a small app with a README that documents the
two working launch patterns (python -m uvicorn app.main:app, and passing the
app object to uvicorn.run) and the pattern that fails, plus a FAQ entry.
tests/test_fastapi_startup.py checks that both working patterns initialise
once, and that the reported pattern and a second init in one process still
raise the same error. No library code changes; the protection against double
initialisation is kept as it is.

Fixes #106
The second-initialisation test only asserted that the subprocess printed a
marker, so a failure reported only a missing marker. Add an explicit else
branch that raises AssertionError with the expected outcome.

Verified both ways: with the guard intact the four startup tests pass; with
the guard removed in a copy, the test fails with the new message.
…he app

The previous test imported app.main and never exercised the documented
uvicorn.run(app) launcher. Add example/fastapi_startup/run.py, the launcher the
README already described, and run it as __main__ with uvicorn.run() replaced by
a recorder. The test checks that uvicorn.run() gets the app object from
app.main, not an import string, and that init_fastapi() runs once. Negative
controls confirm the check fails for a launcher that reloads the app module or
passes an import string.
…ding

- The example README says to run commands from example/fastapi_startup but gave
  a repository-root test path, which finds no tests there. Use
  ../../tests/test_fastapi_startup.py, and mention the root-relative form.
- Document reload and multiple workers: uvicorn requires the import-string
  pattern for both (an app object exits with an error), the app-object pattern
  suits a single process, and init_fastapi() running once per worker process is
  expected.
- Replace the import_from_string() check with the real uvicorn.Config(...).load()
  path that 'python -m uvicorn app.main:app' uses. The test checks the loaded app
  is app.main.app, that init_fastapi() ran once, and that no socket is bound.
@suppagoddo
suppagoddo force-pushed the docs/106-fastapi-init branch from b03cbe8 to fd1f35f Compare October 21, 2026 00:09
@suppagoddo suppagoddo changed the title docs: clarify FastAPI initialisation and Uvicorn startup docs: explain FastAPI double initialization with Uvicorn (fixes #106) Oct 21, 2026
@suppagoddo suppagoddo changed the title docs: explain FastAPI double initialization with Uvicorn (fixes #106) docs: Explain FastAPI double initialization with Uvicorn (fixes #106) Oct 21, 2026
@suppagoddo suppagoddo changed the title docs: Explain FastAPI double initialization with Uvicorn (fixes #106) docs: Explain FastAPI double initialization with Uvicorn Closes #106 (documentation) Oct 21, 2026
@suppagoddo
suppagoddo merged commit 237b46f into master Oct 21, 2026
16 checks passed
suppagoddo added a commit that referenced this pull request Oct 21, 2026
Bump the version to 1.6.0 and collect the changelog into a 1.6.0 section
covering every change merged since 1.5.1:

- Added: json_logging.EXCLUDED_FIELDS (#129, fixes #98)
- Fixed: FastAPI request-context propagation (#123, fixes #124); outside-request
  correlation IDs in Flask, Quart and Connexion (#127, fixes #125); non-web
  get_correlation_id() (#128, fixes #126); Sanic 24+ logging config (#133)
- Documentation: FastAPI/Uvicorn startup (#130, fixes #106); Gunicorn and
  Flask example (#131, fixes #25)
- Compatibility: default output unchanged unless EXCLUDED_FIELDS is set; the
  framework behaviour changes; Python 3.9+ FastAPI context path; CI coverage

The previous 'other integrations unchanged' and 'no change to the JSON schema'
statements are replaced, since they do not hold for the whole release. The
date is added when the release is cut. No library code change.
suppagoddo added a commit that referenced this pull request Oct 21, 2026
* chore(release): prepare json-logging 1.6.0

Bump the version to 1.6.0 and collect the changelog into a 1.6.0 section
covering every change merged since 1.5.1:

- Added: json_logging.EXCLUDED_FIELDS (#129, fixes #98)
- Fixed: FastAPI request-context propagation (#123, fixes #124); outside-request
  correlation IDs in Flask, Quart and Connexion (#127, fixes #125); non-web
  get_correlation_id() (#128, fixes #126); Sanic 24+ logging config (#133)
- Documentation: FastAPI/Uvicorn startup (#130, fixes #106); Gunicorn and
  Flask example (#131, fixes #25)
- Compatibility: default output unchanged unless EXCLUDED_FIELDS is set; the
  framework behaviour changes; Python 3.9+ FastAPI context path; CI coverage

The previous 'other integrations unchanged' and 'no change to the JSON schema'
statements are replaced, since they do not hold for the whole release. The
date is added when the release is cut. No library code change.

* chore(release): credit Bob T. and Udit Agrawal in 1.6.0 metadata
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.

Initialisation error: "Can not call init more than once" [FastAPI]

1 participant