Issue #1309: fix: keep the Query Planner's error body out of GraphQL mutation errors - #1311
Conversation
…mutation errors The update mutation raised with the Query Planner's raw error body, which Strawberry relays to the caller verbatim, and the QP's /update builds that body from str(e) of any exception. The caller now gets only the status (`Mutation failed: <status>`); the body stays in the server log, matching what #1291 did for the query path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merge-order note: #1314 (#1293) removes the module-level |
bjagg
left a comment
There was a problem hiding this comment.
Approving: the change is correct and mirrors #1291. 30 pass; putting response.text back fails the new test, and dropping the status is caught too.
Two non-blocking notes. (1) Nothing guards "the body is still logged": removing the logger.error line passes all 30. Same gap as the query-path test, so consistent, just unguarded. (2) _FakeResponse keeps text and json() independent, so a leak via response.json() would pass these tests. It's fine against real httpx, but deriving json() from text in the fake would close it.
… one schema build (#1314) ##### Description of Change **Problem.** `components/lif/openapi_to_graphql/type_factory.py` declared `input_type_cache` at **module level**, keyed by type name alone, and nothing ever cleared it. So the second schema built in a process reused the first schema's filter input types. Two schemas whose `Person` had entirely different queryable fields silently shared one `PersonInput`. `mutable_input_type_cache`, its sibling, was already per build. As #1293 says, this **isn't reachable in production today**: `bases/lif/api_graphql/core.py` builds the schema once, in `lifespan`. It did already bite tests, though. Two test helpers carried a `monkeypatch.setattr(type_factory, "input_type_cache", {})` workaround, and without it a test passed alone but failed in the suite. It would also turn any future schema hot-reload after an MDR change into a silent bug. **Solution.** This is the issue's suggested change. `generate_graphql_root_types` (`core.py`) now creates `input_type_cache = {}` next to `mutable_input_type_cache` and passes it to `create_input_type`. That gains an `input_type_cache` parameter, matching `create_mutable_input_type`. The module-level variable is gone. `create_input_type` isn't exported from the package (`__init__.py` exports `generate_graphql_root_types` / `generate_graphql_schema`), and `core.py` is its only caller. Both test workarounds are removed: the one the issue names in `test_core.py::TestQueryPlannerFailureReachesCaller`, and a second copy in `test_lif_client_header.py` that #1301 added. Their `_schema` helpers no longer take `monkeypatch`. **Side effects.** None in production. The service builds one schema per process, and it's built the same way. **How reviewers should test it.** ```bash uv run pytest test/components/lif/openapi_to_graphql/ -q ``` ##### Related Issues Closes #1293 Refs #1131 ##### Type of Change - [x] Bug fix (non-breaking change which fixes an issue) ##### Project Area(s) Affected - [x] components/ - [x] test/ or e2e/ --- ##### Checklist - [x] commit message follows commit guidelines (see commitlint.config.mjs) - [x] tests are included (unit and/or integration tests) - [x] code passes linting checks (`uv run ruff check`) - [x] code passes formatting checks (`uv run ruff format`) - [x] code passes type checking (`uv run ty check`) - [x] pre-commit hooks have been run successfully ##### Testing - [x] Automated tests added/updated `TestInputTypesArePerSchemaBuild` builds two schemas in one process: one where `Person` has a single queryable field `alpha`, and one where it's `beta`. It reads each schema's `PersonInput` fields by introspection. - On `main` it **fails** exactly as the issue describes: `assert {'alpha'} == {'beta'}`. The second schema reused the first's input type. - With the fix it passes. The removed workarounds are the third acceptance criterion. With them gone, all 30 tests in `test/components/lif/openapi_to_graphql/` pass in the same run, including the ones that used to need them. `pre-commit run --files` on all four changed files is green, including the full pytest suite. No README or CHANGELOG change: the component README doesn't mention the cache, and nothing changes for a caller. ##### Additional Notes **Merge order with #1311 (#1309), measured by a trial merge.** #1311's new `TestMutationFailureDoesNotLeakBody` carries a third copy of the same workaround line. 1. **Textual conflict in `test_core.py`:** both PRs append a test class at the end of the file. Keep both classes. 2. **Then, a runtime failure git can't see:** once resolved, #1311's test fails with `AttributeError: <module 'lif.openapi_to_graphql.type_factory'> has no attribute 'input_type_cache'`, because `monkeypatch.setattr` refuses a missing attribute. **Delete that one line** (`monkeypatch.setattr(type_factory, "input_type_cache", {})`) and the brick passes (31 passed in the trial). Whichever PR merges second needs both steps. `main` requires branches to be up to date, so that PR's CI will surface step 2 if it's missed. **Shared brick (#1171).** `components/lif/openapi_to_graphql` is packaged by `lif_graphql_api` only, and its deploy workflow already covers the brick. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: dereck <dereck.haskins@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Benito Gonzalez <bgonzalez@unicon.net>
…umn with a 422 (#1313) ##### Description of Change **Problem.** `IdentityMapping`'s string fields have no `max_length`. As #1300 describes, a value longer than its column reaches MariaDB, fails with `1406 Data too long` in strict mode, and returns to the caller as a generic 500 through `DataStoreException`. #1258 lowered the threshold for three fields, from 255 to 191. **Solution: enforce the widths at the API boundary.** The issue left the placement open; this puts it at the API boundary rather than on the shared DTO. - A new `IdentityMappingRequest(IdentityMapping)` in `bases/lif/identity_mapper_restapi/core.py` is the `POST .../mappings` body type. Each of the five string fields carries `max_length` **read from the SQLAlchemy column** (`IdentityMappingModel.__table__.c[name].type.length`). The model already mirrors `02-ddl.sql`, so there's no third copy of the numbers. - The endpoint converts the validated items back to plain `IdentityMapping` before calling the service. The request model only validates; the service and storage receive exactly what they did before. This also keeps `list[IdentityMapping]` type-correct for `ty` (`list` is invariant) without widening the service's signature. - An oversized field now gets FastAPI's standard `422`, with `loc` naming it, e.g. `["body", 0, "target_system_id"]`. **Why not `max_length` on the DTO in `components/lif/datatypes`.** Only the Identity Mapper bricks use `IdentityMapping`, but `datatypes` is packaged by 12 projects and watched by 9 deploy workflows. Putting it there would redeploy about nine services for a class one of them uses, and it would be a third copy of the widths. Here, only the Identity Mapper redeploys. **Limitations.** `max_length` counts characters, the same unit as MariaDB's `VARCHAR(n)`. The 500 this replaces is as the issue describes it; I didn't reproduce it against a real MariaDB for this PR. The tests prove the 422 at the API, which is the part this change owns. The `GET`/`DELETE` path parameters aren't limited: an over-long one just matches no row. **How reviewers should test it.** ```bash uv run pytest test/bases/lif/identity_mapper_restapi/ -q -k column ``` ##### Related Issues Closes #1300 Refs #1131 ##### Type of Change - [x] Bug fix (non-breaking change which fixes an issue) ##### Project Area(s) Affected - [x] bases/ - [x] test/ or e2e/ - [x] API endpoints - [x] Documentation (docs/, READMEs, ARCHITECTURE.md, CLAUDE.md) --- ##### Checklist - [x] commit message follows commit guidelines (see commitlint.config.mjs) - [x] tests are included (unit and/or integration tests) - [x] code passes linting checks (`uv run ruff check`) - [x] code passes formatting checks (`uv run ruff format`) - [x] code passes type checking (`uv run ty check`) - [x] pre-commit hooks have been run successfully - [x] API changes: base README updated (`bases/lif/identity_mapper_restapi/README.md`). No page under `docs/` documents the Identity Mapper's status codes ##### Testing - [x] Automated tests added/updated Two tests, each parametrized over all five fields, with the widths pinned as literals from the issue's table (191 / 191 / 191 / 100 / 255). A width change then shows up as a deliberate test edit. | Test | `main` | This branch | |---|---|---| | One character over the width → `422`, error `loc` is exactly `["body", 0, <field>]`, service never called | **fails** ×5 (reaches the service, 200) | passes ×5 | | Exactly at the width → `200`, service receives a plain `IdentityMapping` | passes ×5 | passes ×5 | With only `core.py` reverted to `main`, the five "over" cases fail again. The three existing save tests, which compare the service's call arguments against `IdentityMapping(...)`, pass **unchanged**, which confirms the service still gets the plain DTO. The identity-mapper base, service and storage suites pass (86 tests). `pre-commit run --files` on all four changed files is green, including the full pytest suite. ##### Additional Notes **Merge order with #1312** (#1261, the 409 on a persistent collision). I trial-merged the two branches: - `core.py` and the test file **merge cleanly**. - **`CHANGELOG.md`** and **`bases/lif/identity_mapper_restapi/README.md`** conflict: both PRs add a paragraph at the same spot. Keep both. `CHANGELOG.md` is also shared with #1310, #1311 and #1148, under the same keep-both rule. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: dereck <dereck.haskins@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Benito J. Gonzalez <bgonzalez@unicon.net>
Resolve CHANGELOG.md with #1310 and #1313 by keeping every entry. In test_core.py, keep main's file plus this branch's TestMutationFailureDoesNotLeakBody, minus its monkeypatch.setattr(type_factory, "input_type_cache", {}) line: #1314 removed that module attribute, so the line raised AttributeError (the step #1314's PR body describes).
Description of Change
Problem. The GraphQL update mutation raised
Exception(f"Mutation failed: {response.status_code}: {response.text}")(components/lif/openapi_to_graphql/type_factory.py:991). Strawberry puts that message into theerrorsentry the caller receives. The Query Planner's/updatehandler builds its 500 body fromstr(e)of any exception it catches (bases/lif/query_planner_restapi/core.py:287-288). So whatever a backend exception said (a driver message, a host, a username) reached whoever held a GraphQL API key.This is the leak bjagg measured on the query path in #1291, where a credential-shaped 500 reached the caller as
Query failed: 500: {"detail":"FATAL: password authentication failed for user \"lifadmin\" host=10.0.3.17"}. His approval left the mutation's copy as a follow-up, filed as #1309.Solution. The same one-line change #1291 made to the query path: drop
: {response.text}from the raised message. Thelogger.errorline keeps the full body for operators, and a comment points back at the query path. The caller still gets the status (Mutation failed: 500), so a failed mutation stays clearly a failure.Side effects. Only the error text changes. The MCP
lif_mutationtool passes on the GraphQL error, so its tool error gets shorter. The body is no longer available to the caller for debugging; it's in the GraphQL server log instead.How reviewers should test it.
To confirm the test guards the change: put
: {response.text}back into the raise and the test fails onassert "mongodb-org1" not in result.errors[0].message.Related Issues
Closes #1309
Refs #1131
Type of Change
Project Area(s) Affected
Checklist
uv run ruff check)uv run ruff format)uv run ty check)Testing
TestMutationFailureDoesNotLeakBodybuilds a real schema with a mutable field, stubshttpx.AsyncClientwith the same fake the #1264 tests use, and runsupdatePersonagainst a 500 whose body namesmongodb-org1:27017. It asserts that the caller gets an error, that the error includes500, and that the host name isn't in it.mainit fails on the host assertion (the message wasMutation failed: 500: {"detail":"connection refused: mongodb-org1:27017"}), having already passed the schema build and the500check. So it fails for the right reason.test/components/lif/openapi_to_graphql/.pre-commit run --fileson all four changed files is green, including the full pytest suite.The
api_graphqlREADME's error contract now says the message carries only the status, and that the Query Planner body goes to the server log. There's also a CHANGELOG entry.Additional Notes
Shared brick (#1171).
components/lif/openapi_to_graphqlis packaged bylif_graphql_apionly, and the GraphQL deploy workflow already covers it (the #1291 and #1301 merges both redeployed GraphQL).Merge order. This PR, #1310 and #1148 each add a line at the top of
CHANGELOG.md's[Unreleased]→### Changedlist. Whichever merges later keeps both lines. There's no other file overlap with open PRs.🤖 Generated with Claude Code