refactor(inner_api): dep-inject request payloads with @model_validate - #41575
refactor(inner_api): dep-inject request payloads with @model_validate#41575ShousenZHANG wants to merge 2 commits into
Conversation
Replace the manual `Model.model_validate(inner_api_ns.payload or {})` calls in
the inner API controllers with the `@model_validate` decorator, so the payload
is validated and injected before the handler body runs.
Sites converted:
- inner_api/runtime_credentials.py: InnerRuntimeCredentialsResolvePayload
- inner_api/app/dsl.py: InnerAppDSLImportPayload
- inner_api/mail.py: InnerMailPayload
The existing tests drive these handlers through `inspect.unwrap`, which skips
every decorator, so they left the new layer unexercised. Added tests that call
the handlers normally and assert a 422 for an invalid body, plus coverage for
`EnterpriseMail`/`BillingMail`, whose `super().post()` calls now rely on the
decorator to supply the payload.
Pyrefly Diffbase → PR--- /tmp/pyrefly_base.txt 2026-09-01 08:54:24.554381864 +0000
+++ /tmp/pyrefly_pr.txt 2026-09-01 08:54:16.494345399 +0000
@@ -3581,7 +3581,7 @@
ERROR `SimpleNamespace` is not assignable to attribute `request` with type `Request` [bad-assignment]
--> tests/unit_tests/controllers/files/test_upload.py:414:26
ERROR Object of class `FunctionType` has no attribute `__apidoc__` [missing-attribute]
- --> tests/unit_tests/controllers/inner_api/app/test_dsl.py:259:18
+ --> tests/unit_tests/controllers/inner_api/app/test_dsl.py:261:18
ERROR Missing argument `tenant_model` in function `protected_view` [missing-argument]
--> tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py:257:40
ERROR Missing argument `user_model` in function `protected_view` [missing-argument]
|
Pyrefly Type Coverage
|
There was a problem hiding this comment.
Pull request overview
This PR continues the #36659 refactor campaign by replacing remaining inline Model.model_validate(inner_api_ns.payload or {}) parsing in inner_api controllers with the shared @model_validate decorator. This standardizes request validation, injects validated Pydantic models into handlers, and ensures invalid bodies consistently surface as 422 UnprocessableEntity from the decorator layer.
Changes:
- Converted three inner API handlers to use
@model_validate(...)and accept the validated payload model as the first parameter afterself. - Updated unit tests to stop patching
inner_api_ns.payloadand instead validate payloads explicitly when calling unwrapped handlers. - Added new tests that exercise the real
@model_validatebehavior (raisingUnprocessableEntityfor invalid bodies) for each converted site, includingEnterpriseMail/BillingMailforwarding viasuper().post().
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| api/controllers/inner_api/runtime_credentials.py | Switches runtime credentials resolver to decorator-based payload validation/injection. |
| api/controllers/inner_api/app/dsl.py | Switches DSL import handler to decorator-based payload validation/injection while preserving existing export validation behavior. |
| api/controllers/inner_api/mail.py | Switches BaseMail to decorator-based payload validation/injection so subclasses’ super().post() continues to work without manual parsing. |
| api/tests/unit_tests/controllers/inner_api/test_runtime_credentials.py | Updates tests for new handler signature and adds a decorator-path invalid-body assertion (422). |
| api/tests/unit_tests/controllers/inner_api/app/test_dsl.py | Updates import tests for new handler signature and adds a decorator-path invalid-body assertion (422). |
| api/tests/unit_tests/controllers/inner_api/test_mail.py | Updates BaseMail tests to use request JSON (not patched namespace payload) and adds invalid-body + subclass forwarding coverage. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…y_config `tests/unit_tests/test_config_overrides.py` bans direct `patch.object(dify_config, ...)` in the unit-test tree, which my two new decorator tests were doing. Switch them to the sanctioned `config_overrides_context` helper, matching `test_agent_llm.py` in the same directory. It also validates the field names, which the raw patch did not.
|
Two corrections to the description above, plus a note on the CI failure. 1. The first push failed CI, and my local verification could not have caught it. The reason I missed it: I ran 2. "the remaining manual 3. My exclusion list got two things wrong.
Sorry for the noise. The controller change itself is unchanged from the first push. |
Summary
Part of #36659. Converts the
Model.model_validate(inner_api_ns.payload or {})callsin three inner API controllers to the
@model_validatedecorator, following#41374 / #41539.
api/controllers/inner_api/runtime_credentials.pyEnterpriseRuntimeCredentialsResolve.postInnerRuntimeCredentialsResolvePayloadapi/controllers/inner_api/app/dsl.pyEnterpriseAppDSLImport.postInnerAppDSLImportPayloadapi/controllers/inner_api/mail.pyBaseMail.postInnerMailPayloadThe decorator is applied innermost, below the
@inner_api_ns.expect(...)docsdecorators, and the model becomes the first parameter after
self— matchingthe merged sites in this campaign.
These are the three sites I claimed in
#36659 (comment).
What is left in
inner_apiafter this PR, so the scope is explicit:workspace/workspace.pyL58, L101, L140 — covered by refactor(inner_api): dep-inject workspace payloads with @model_validate #41540.agent/llm.py,agent/tools.py,agent/files.py(two sites),knowledge/retrieval.py— each wraps its parse in
except ValidationErrorand raises a custom 400, which thedecorator's 422 would change.
agent/{llm,tools}.pyandknowledge/retrieval.pyhavetests asserting that branch;
agent/files.pydoes not, but it raisesAgentFileRequestHttpError(..., status_code=400)all the same.app/dsl.py:110is arequest.argsGET site that returns a 400 body, not thens.payloadshape, so it is out of scope here either way.Test coverage
The existing tests for these handlers call them through
inspect.unwrap, whichstrips every decorator and passes an already-validated model in. That means a
straight conversion would have left the new layer completely unexercised, so
this PR adds tests that call the handlers the normal way:
UnprocessableEntity(422) for an invalid body,driven through the real decorator stack rather than an unwrapped view;
EnterpriseMailandBillingMailforward toBaseMail.postvia ano-argument
super().post(), which now depends on the decorator to supply thepayload — added a valid-body and an invalid-body test for both, since they
previously had no behavioural test for
postat all.Each of the three decorators was mutation-checked: deleting it turns the
corresponding tests red (1, 1, and 7 failures respectively).
The two tests that drive the real stack need
INNER_API/INNER_API_KEYset, and useconfig_overrides_contextfor that — directpatch.object(dify_config, ...)is banned bytests/unit_tests/test_config_overrides.py. My first push got that wrong and went red; seethe comment below.
Verification
Run locally. Note the scope: everything below is scoped to the changed surface, so it
cannot see failures elsewhere in the suite — CI is the authority, not this list.
pytest api/tests/unit_tests/controllers/inner_api/— 159 passed (152 onmain, +7 new)pytest api/tests/unit_tests/test_config_overrides.py— the AST guard reports no violationanywhere in the unit-test tree
ruff check/ruff format --check— cleanpyrefly check controllers/inner_api/— 0 diagnosticspyrefly check --config tests/unit_tests/pyrefly.toml tests/unit_tests/controllers/inner_api/— 354 diagnostics, identical to themainbaseline (0 new)mypy --check-untyped-defs --disable-error-code=import-untyped controllers/inner_api/— noissues (that is the flag set
make type-check-coreuses; without it you get the usual 205pre-existing
import-untypedstub warnings)lint-imports— 23 contracts kept, 0 brokendev/lint_response_contracts.py --fail-on-mismatch— 0 mismatchgetattr(in the diffBehaviour note
As on the earlier PRs in this campaign, invalid bodies now surface as 422
(
UnprocessableEntity) from the decorator rather than whatever the handlerpreviously raised. That is the campaign's intended behaviour.
This PR was written with AI assistance.