# Reliability & Accuracy

> **Status:** v0.1 — first pass, finalized 2026-05-15 (review-pass
> verified all `file:line` citations against the live source).
> A federation-sibling-scoped reliability audit: walked the five
> source files (`_apps.py`, `_middleware.py`, `_logging.py`,
> `_settings.py`, `apps.py`) end-to-end against the **RA** /
> **EH** / **SC** rule set from `mgf-standard/docs/standards/`,
> verified every claim against `file:line`, identified the
> systemic patterns. The sibling's public surface is five names
> (`MgfCommonConfig`, `MgfRequestIdMiddleware`,
> `MgfContextMiddleware`, `JsonFormatter`,
> `DjangoSettingsBridge`); the audit scope is correspondingly
> narrow.
>
> **Scope:** v1.0 readiness for mgf-django. Items beyond v1.0
> are noted but deprioritised; revisit when the v1.0 surface-lock
> review opens.
> **Audience:** the maintainer + future AI agents doing
> reliability work on this sibling.
> **Cross-references:** [`PUBLIC_API.md`](../../PUBLIC_API.md),
> [`docs/standards/README.md`](../standards/README.md),
> [`mgf-common/docs/inprogress/RELIABILITY_AND_ACCURACY.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/inprogress/RELIABILITY_AND_ACCURACY.md)
> (the cornerstone's tracker — the patterns documented here are
> sibling-scoped versions of the cross-cutting invariants
> tracked there).

This document is the **living priority tracker** for reliability
+ accuracy work in `mgf-django`. The shape mirrors mgf-common's
canonical tracker shape: priority table at the top, work-unit
detail below, status flipped as items close.

The sibling is at v0.2.0 (maiden release post-v2.6/v2.7
compliance, with v0.1.0 prepped but never published — see
`CHANGELOG.md`). The surface is small but it sits on the
request path of every Django consumer; a reliability gap here
costs every downstream app.

---

## §1 You are here

mgf-django ships five public names; 18 unit tests carried over
from the pre-extraction state in mgf-common; mypy --strict
clean; ruff clean. Surface quality is high.

What this tracker exposes is the **per-request reliability
shape** — the invariants that hold under nominal Django
deployment but might not under adversarial input
(`X-Request-Id` header injection), test-runner concurrency
(parallel `override_settings(INSTALLED_APPS=...)` blocks),
or upstream library refactors (`mgf.common.current_context`
growing a new exception type).

**Counts (2026-05-17):**

| Priority | Open | Closed | Total |
|---|---|---|---|
| P0 — v1.0 surface-lock blockers | 0 | 0 | 0 |
| P1 — should land before/with v1.0 | 0 | 3 | 3 |
| P2 — post-v1.0 hardening | 0 | 3 | 3 |
| **Total** | **0** | **6** | **6** |

**Do first:** ALL items closed. RA-01..03 (P1, 2026-05-17).
RA-04 / RA-05 / RA-06 (P2, 2026-05-17 — `_str_or_none` tightened
to reject non-str, both middlewares declare `async_capable=False`
with docstring note, `MGF_DJANGO_REDACT_EXTRA_KEYS` env-var seam
shipped on `JsonFormatter`).

---

## §2 Priority table

| ID | Concern | One-line | Effort | Status |
|---|---|---|---|---|
| [**RA-01**](#ra-01-header-injection-on-x-request-id-reflect) | Security / input validation | `MgfRequestIdMiddleware` echoes incoming `X-Request-Id` header without sanitization — a client-supplied `\r\n` can inject downstream headers (Set-Cookie, redirect Location). | S | ✅ closed |
| [**RA-02**](#ra-02-silent-swallow-in-mgfcontextmiddleware) | Error-handling rule alignment (EH-10) | `MgfContextMiddleware.__call__` does `try / except Exception: ctx = None` — hides every failure mode of `current_context()`. | S | ✅ closed |
| [**RA-03**](#ra-03-no-concurrent-bootstrap-test) | Test coverage (RA pattern from mgf-common) | The module-level `_BOOTSTRAP_LOCK` + `_BOOTSTRAP_STACK` guard is correct by inspection, but no test fires N threads at `MgfCommonConfig().ready()` to pin the invariant. | S | ✅ closed |
| [**RA-04**](#ra-04-djangosettingsbridge-coerces-non-string-types) | Defensive validation | `_str_or_none(value)` in `_settings.py` calls `str(value)` on any non-None input — a misconfigured `MGF_COMMON_APP_NAME = ['foo']` ends up in the bridge as `"['foo']"`. | S | ✅ closed |
| [**RA-05**](#ra-05-async-middleware-discipline-documented) | Documentation / contract | Both middleware classes are sync-mode-only — no `async_capable = True`. Mixed async Django apps silently pay Django's sync-to-async adapter cost per request. | XS | ✅ closed |
| [**RA-06**](#ra-06-jsonformatter-redactor-injection-ergonomics) | Consumer ergonomics | `JsonFormatter()` constructs `Redactor.default()` with no customization seam. Consumers wanting a custom `extra_keys` MUST subclass — documented in the docstring but easy to miss. | S | ✅ closed |

Effort scale: XS (≤ 15 min); S (≤ 1 h); M (≤ 1 session); L
(multiple sessions).

---

## §3 Invariants we hold

The list of "closed elsewhere; do not re-open" — facts the
audit confirmed about the current state that future passes
should NOT re-investigate without new evidence:

- **RA-INV-1** — Bootstrap is **single-shot per process**.
  `_BOOTSTRAP_STACK` module-level guard + `threading.Lock`
  (`src/mgf/django/_apps.py:33-34`). Double-`ready()` (e.g.,
  test reset, `override_settings(INSTALLED_APPS=...)`) is a
  no-op (`_apps.py:78-83`). Correct by inspection; the
  remaining concern is just the missing test (RA-03).
- **RA-INV-2** — Contextvar reset is in `finally`.
  `MgfRequestIdMiddleware.__call__` uses `try/finally` around
  `_REQUEST_ID_VAR.reset(token)` (`_middleware.py:60-65`).
  Exception-path correctness is maintained; the response
  header at line 67 isn't set on the exception path, which
  is correct (Django's exception middleware handles the
  response).
- **RA-INV-3** — `SECRET_KEY` itself is **never** carried in
  the bridge. `DjangoSettingsBridge.from_django()` reads
  `settings.SECRET_KEY` only to compute `bool(secret_key)`
  (`_settings.py:79+87`); the string is discarded.
  `secret_key_present` is the only signal. `mgf-common
  explain` output is safe to share.
- **RA-INV-4** — `atexit.register(_cleanup_bootstrap)` fires
  ONCE per process (gated by the same `_BOOTSTRAP_STACK is
  not None` check inside `_cleanup_bootstrap`). Multiple
  `ready()` calls don't multi-register; the cleanup is
  idempotent.
- **RA-INV-5** — Django app **label** is `mgf_django` (not
  `mgf.django` — dots forbidden by Django's app-registry).
  Renamed from `mgf_common` at the v0.1 sibling extraction;
  consumer config keying off the old label is documented in
  [`docs/cutover/v0.1.0.md`](../cutover/v0.1.0.md) §"The
  label rename".

---

## §4 Work units

### RA-01 Header-injection on `X-Request-Id` reflect

**Why:** `MgfRequestIdMiddleware.__call__`
([`_middleware.py:54`](../../src/mgf/django/_middleware.py#L54))
reads the incoming `HTTP_X_REQUEST_ID` from `request.META`,
uses it as-is if non-empty, and echoes it on the response at
([`_middleware.py:67`](../../src/mgf/django/_middleware.py#L67)):

```python
response[REQUEST_ID_HEADER] = request_id
```

If the client sends:

```
X-Request-Id: foo\r\nSet-Cookie: malicious=1
```

Django's WSGI layer typically rejects CR/LF in header values
on the inbound path, but not always (depends on the WSGI
server: `gunicorn` rejects; `uwsgi` may not). On the outbound
echo, the value is passed to Django's `HttpResponse.__setitem__`
which does **bad-header detection** (raises `BadHeaderError`
on CR/LF) — so the immediate exposure is limited to a 500
error.

**The real concern:** Django's `BadHeaderError` is raised
DURING response construction, AFTER the view has run + maybe
committed DB writes / sent emails. The middleware should
reject the malformed input upstream, returning the request to
a clean state without spending a full view dispatch.

**Concrete output:**

- A guard at `_middleware.py:54-56` that:
  - Rejects values containing `\r`, `\n`, or `\0`.
  - Caps length at a documented bound (e.g., 128 chars).
  - On rejection: generate a fresh UUID4 and proceed (don't
    abort the request — drop the bad value, keep going).
- A unit test in `tests/unit/test_middleware.py` that:
  - Sends `X-Request-Id: foo\r\nX-Injected: yes` and asserts
    the response's `X-Request-Id` is a fresh UUID (not the
    crafted value).
  - Sends `X-Request-Id: <128-char string>` and asserts the
    incoming value is kept; sends `<129-char string>` and
    asserts a fresh UUID is used.

**Locked in by:** the new unit test asserts both the
rejection-and-regenerate behaviour and the length cap.

**Cross-references:** SC-09 (sink redaction — the request-id
itself isn't a secret, but the principle of validating
client-supplied identifiers before reflection applies);
mgf-fastapi's `RequestIdMiddleware` has the same surface and
SHOULD adopt the same guard (file a follow-up paper if so).

---

### RA-02 Silent-swallow in `MgfContextMiddleware`

**Why:** `MgfContextMiddleware.__call__`
([`_middleware.py:107-110`](../../src/mgf/django/_middleware.py#L107)):

```python
try:
    ctx = current_context()
except Exception:
    ctx = None
```

The bare `except Exception` hides every failure mode of
`current_context()`. The docstring claims this is to handle
"the middleware loaded but bootstrap hasn't run" — which is a
valid case (returns `LookupError`) — but a future refactor of
`mgf.common._lifecycle` introducing a new exception type
(e.g., a `BootstrapStateError` for partial-init) would be
silently swallowed.

Per **EH-04** + **EH-10**: the "best-effort observer" pattern
is allowed only when the failure is audit-logged. A bare
silent except is forbidden.

**Concrete output:**

- Tighten the catch to `LookupError` (the documented
  pre-bootstrap case) at `_middleware.py:107-110`.
- For any other exception, log a WARNING-level audit event
  via `mgf.common`'s standard logger and re-raise (let
  Django's exception middleware handle).
- Update the docstring to say "raises if `current_context()`
  raises a non-LookupError" — that's the new contract.

**Locked in by:** a unit test in `tests/unit/test_middleware.py`
that monkeypatches `current_context` to raise a custom
exception subclass and asserts (a) the middleware re-raises,
(b) the audit log line is present.

**Cross-references:** EH-04 (top-level handlers), EH-10
(best-effort observers).

---

### RA-03 No concurrent-bootstrap test

**Why:** The module-level `_BOOTSTRAP_LOCK` +
`_BOOTSTRAP_STACK` guard in
[`_apps.py:33-34`](../../src/mgf/django/_apps.py#L33) +
the `with _BOOTSTRAP_LOCK:` block at
[`_apps.py:77-94`](../../src/mgf/django/_apps.py#L77) is
correct by inspection — but no test fires N threads at
`MgfCommonConfig().ready()` to pin the invariant. mgf-common's
RA-03 closed the same gap for the parent's `bootstrap()` (per
[`mgf-common/docs/inprogress/RELIABILITY_AND_ACCURACY.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/inprogress/RELIABILITY_AND_ACCURACY.md));
this sibling-side wrapper SHOULD have parity.

The test isn't theoretical — Django's app-registry IS single-
threaded under normal `manage.py runserver`, but:

- pytest-django's `--reuse-db` workflow constructs the app
  registry per worker; with `pytest-xdist -n 4` four
  processes race on first import.
- Test-only `override_settings(INSTALLED_APPS=...)` can call
  `ready()` mid-test from a different thread than the one
  that did the original load (rare but happens).
- Future Django async-views work may re-enter `ready()` from
  the asgi worker pool.

**Concrete output:**

- Add `tests/unit/test_apps_concurrency.py` with a single
  test that:
  - Resets `_BOOTSTRAP_STACK = None` (via the test reset
    pathway documented in `_cleanup_bootstrap`).
  - Fires N=20 threads at `MgfCommonConfig().ready()`.
  - Joins all threads.
  - Asserts the bootstrap counter (a fresh `mock.MagicMock`
    monkey-patched onto `mgf.common._lifecycle.bootstrap`)
    was called **exactly once**.
- Skip the test if `threading` import is restricted in the
  test env (it isn't, but defensive).

**Locked in by:** the new test fails on first run if the
guard ever regresses (e.g., a future refactor that loses the
lock).

**Cross-references:** mgf-common RA-03 (concurrent bootstrap
on logging stack — closed).

---

### RA-04 `DjangoSettingsBridge` coerces non-string types

**Why:** `_str_or_none` in
[`_settings.py:93-97`](../../src/mgf/django/_settings.py#L93):

```python
def _str_or_none(value: Any) -> str | None:
    if value is None:
        return None
    return str(value)
```

A misconfigured `settings.MGF_COMMON_APP_NAME = ["foo"]`
silently becomes `mgf_app_name = "['foo']"` in the bridge. The
bridge then ships that stringified-repr to `mgf-common explain`
output and to any downstream consumer reading the field.

The pydantic field `mgf_app_name: str | None = None` would
catch a non-str / non-None value on direct construction —
but `_str_or_none` is the bypass that defeats the validation.

**Concrete output:**

- Tighten `_str_or_none` to accept only `str | None`; raise
  `TypeError` on other types.
- Update the call sites in `from_django()` to pass through
  pydantic's validation (i.e., remove the coercion — let
  pydantic reject mis-typed config at construction).
- Add a unit test in `tests/unit/test_settings_bridge.py`
  that:
  - Sets `MGF_COMMON_APP_NAME = ["foo"]` via `override_settings`.
  - Asserts `DjangoSettingsBridge.from_django()` raises a
    typed validation error.

**Locked in by:** the test fails on first run if the
coercion regresses.

**Cross-references:** CF-06 (pydantic validates types at
boundary), DP-04 (mypy --strict — `Any` here is a smell).

---

### RA-05 Async middleware discipline documented

**Why:** Both `MgfRequestIdMiddleware`
([`_middleware.py:25`](../../src/mgf/django/_middleware.py#L25))
and `MgfContextMiddleware`
([`_middleware.py:71`](../../src/mgf/django/_middleware.py#L71))
are sync-mode-only — they don't set `async_capable = True`
nor implement `async def __call__`. Django's middleware
chain auto-adapts sync middleware into the async path via
`sync_to_async` / `async_to_sync` shims; the cost is a
thread-pool round-trip per request.

For a Django app running synchronous views, no impact. For a
Django app running `async def` views, every request pays the
shim cost. The sibling's docstring doesn't call this out.

**Concrete output:**

- Add a clear note to both middleware classes' docstrings:
  "Sync-mode only — async Django apps incur Django's
  sync-to-async adapter overhead. Track [link to a future
  async-shaped follow-up paper] for the async-capable
  variant."
- File the async-shape follow-up as an inprogress item OR a
  FEEDBACK paper in `mgf-common/FEEDBACK.md` so a future
  consumer with real async-views perf data can prioritise.
- Optionally: add `async_capable = False` explicitly to both
  middleware classes (the default is implicit-False; making
  it explicit is documentation).

**Locked in by:** the docstring change is review-level; the
explicit `async_capable = False` is the mechanical part.

**Cross-references:** Django docs on async support; a
potential mgf-fastapi parity discussion (the FastAPI sibling
is async-native; the contract surface between them is the
shared contextvar).

---

### RA-06 `JsonFormatter` redactor-injection ergonomics

**Why:** `JsonFormatter.__init__`
([`_logging.py:58-59`](../../src/mgf/django/_logging.py#L58)):

```python
def __init__(self) -> None:
    super().__init__(redactor=Redactor.default())
```

The no-arg constructor is required for Django's
`LOGGING['formatters']['()']` factory syntax. But a consumer
wanting custom redaction keys (e.g., to add a project-specific
secret header name to the redaction set) MUST subclass — the
docstring shows the pattern, but it's easy to miss.

The mgf-common parent's `Redactor` accepts `extra_keys=` so the
customization point exists; the Django wrapper has no seam
exposing it.

**Concrete output:**

Two options, pick one (or document both):

1. **Env-var-driven extension** — read
   `MGF_DJANGO_REDACT_EXTRA_KEYS` (comma-separated) at
   `__init__` time. Zero-config for consumers; documented in
   the docstring.
2. **Class-attribute hook** — let consumers subclass and set
   `extra_keys: frozenset[str] = frozenset()` at the class
   level, with `__init__` reading the class attr. Slightly
   more idiomatic Django pattern (matches `AppConfig`-style
   subclassing).

Either way: the docstring should clearly state the
customization path. A recipe in
[`docs/recipes/`](../recipes/) titled
`how-to-add-extra-redaction-keys.md` would help discoverability.

**Locked in by:** a unit test that constructs the formatter
with the customization in place and asserts a custom key is
redacted from a sample log record.

**Cross-references:** LG-04 (sensitive-field redaction
configurable per project), SC-09 (sink redaction).

---

## §5 Closed items

- **RA-01** — Closed-by-reference (SH-01). The X-Request-Id
  injection guard shipped under the SH commit `b95bae3`
  (`sec(django): close SH-01..SH-03`). The defect is dual-
  framed (security + reliability) so the fix lands once; the
  RA tracker records the close. See SH-01 in
  `SECURITY_HARDENING.md` for the full description: a
  `_sanitise_request_id` helper rejects CR/LF/NUL/control-chars
  + values longer than 128 chars; rejected values fall through
  to a fresh UUID; 5 unit tests pin the contract. (2026-05-16,
  commit `b95bae3` — recorded here for tracker-coverage parity.)
- **RA-02** — Tightened the silent-swallow in
  `MgfContextMiddleware.__call__`. Previously
  `except Exception: ctx = None` hid every failure mode of
  `current_context()` — including a hypothetical new exception
  type from a future `mgf.common._lifecycle` refactor.
  Now: `LookupError` (the documented pre-bootstrap case) still
  sets `ctx = None` and continues; ANY other exception type
  fires a WARNING audit-log line (`audit=True`,
  `event="mgf_django.context_middleware_unexpected_error"`,
  `exc_info=True`) and re-raises. Operators see the new
  failure mode in their error tracker instead of a silent
  `request.mgf_context = None` everywhere. Two tests pin the
  contract: LookupError → ctx=None; custom exception → audit-
  log + re-raise. (2026-05-17, commit pending.)
- **RA-03** — N-thread concurrent-bootstrap test. New
  `tests/unit/test_apps_concurrency.py` with two tests: 20
  threads simultaneously calling `MgfCommonConfig().ready()`
  (via the Django app registry) results in EXACTLY ONE
  `mgf.common.bootstrap` invocation, AND the same single-shot
  guarantee under sequential double-`ready()`. The test uses
  a barrier-coordinated thread pool to force racing, a
  counted-no-op `_fake_bootstrap` context manager to record
  invocations, and a `_reset_bootstrap_guard()` helper that
  walks the module-level `_BOOTSTRAP_STACK` cleanly between
  tests. A regression that drops `_BOOTSTRAP_LOCK` or re-
  enters `bootstrap` would record N invocations — the test
  fails with a message naming the broken contract.
  (2026-05-17, commit pending.)

---

## §6 Notes on methodology

This first pass walked the five source files end-to-end (~440
LOC total) against:

- **RA** rule patterns from mgf-common's
  `inprogress/RELIABILITY_AND_ACCURACY.md` — atomic writes,
  lock discipline, rollback semantics, exception swallowing,
  doc drift after deprecation removal, idempotency, cross-
  platform paths.
- **EH** rules from `mgf-standard/docs/standards/ERROR_HANDLING.md`
  — translation seams, top-level handlers, best-effort
  observers, async error handling.
- **SC** rules from `mgf-standard/docs/standards/SECURITY_STANDARD.md`
  — input validation at boundaries, secret-key handling,
  header-injection class.

The audit method:

1. Read each source file end-to-end with the rule set in mind.
2. For every concern surfaced, verify `file:line` against the
   live source.
3. Cross-reference against mgf-common's RA tracker — every
   sibling-side pattern that has a cornerstone analog should
   benefit from the parent's prior work.
4. Triage: a concern that doesn't tie to a rule + concrete
   evidence is an "observation," not a tracker item — those
   go in a parking-lot section or get dropped.

The pattern of "small sibling, narrow surface" makes this
audit cheaper than the cornerstone's — 6 items vs the
parent's 50+. Future passes should look for:

- **Pattern parity with the parent.** When mgf-common closes
  a systemic invariant (e.g., the atomic-write helper),
  check whether the sibling needs the same shape.
- **Test-coverage gaps for invariants verified by inspection.**
  RA-03 is the example — "correct by inspection but no test."
  Sibling test suites tend to be smaller and these gaps are
  easier to spot than in the cornerstone.
- **Documentation drift after a rename.** The Django-label
  rename (RA-INV-5) is the closed example; future renames
  will create the same drift if the audit doesn't catch them.

When an item closes, move it to §5 with the closing-commit
SHA. When the next pass sharpens an item, rewrite it in place
and note the change in §1.
