Skip to content

refactor(inner_api): dep-inject request payloads with @model_validate - #41575

Open
ShousenZHANG wants to merge 2 commits into
langgenius:mainfrom
ShousenZHANG:refactor/dep-inject-inner-api-rest
Open

refactor(inner_api): dep-inject request payloads with @model_validate#41575
ShousenZHANG wants to merge 2 commits into
langgenius:mainfrom
ShousenZHANG:refactor/dep-inject-inner-api-rest

Conversation

@ShousenZHANG

@ShousenZHANG ShousenZHANG commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of #36659. Converts the Model.model_validate(inner_api_ns.payload or {}) calls
in three inner API controllers to the @model_validate decorator, following
#41374 / #41539.

File Handler Payload
api/controllers/inner_api/runtime_credentials.py EnterpriseRuntimeCredentialsResolve.post InnerRuntimeCredentialsResolvePayload
api/controllers/inner_api/app/dsl.py EnterpriseAppDSLImport.post InnerAppDSLImportPayload
api/controllers/inner_api/mail.py BaseMail.post InnerMailPayload

The decorator is applied innermost, below the @inner_api_ns.expect(...) docs
decorators, and the model becomes the first parameter after self — matching
the merged sites in this campaign.

These are the three sites I claimed in
#36659 (comment).

What is left in inner_api after this PR, so the scope is explicit:

  • workspace/workspace.py L58, 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 ValidationError and raises a custom 400, which the
    decorator's 422 would change. agent/{llm,tools}.py and knowledge/retrieval.py have
    tests asserting that branch; agent/files.py does not, but it raises
    AgentFileRequestHttpError(..., status_code=400) all the same.
  • app/dsl.py:110 is a request.args GET site that returns a 400 body, not the
    ns.payload shape, so it is out of scope here either way.

Test coverage

The existing tests for these handlers call them through inspect.unwrap, which
strips 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:

  • one test per site asserting UnprocessableEntity (422) for an invalid body,
    driven through the real decorator stack rather than an unwrapped view;
  • EnterpriseMail and BillingMail forward to BaseMail.post via a
    no-argument super().post(), which now depends on the decorator to supply the
    payload — added a valid-body and an invalid-body test for both, since they
    previously had no behavioural test for post at 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_KEY set, and use
config_overrides_context for that — direct patch.object(dify_config, ...) is banned by
tests/unit_tests/test_config_overrides.py. My first push got that wrong and went red; see
the 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 on main, +7 new)
  • pytest api/tests/unit_tests/test_config_overrides.py — the AST guard reports no violation
    anywhere in the unit-test tree
  • ruff check / ruff format --check — clean
  • pyrefly check controllers/inner_api/ — 0 diagnostics
  • pyrefly check --config tests/unit_tests/pyrefly.toml tests/unit_tests/controllers/inner_api/ — 354 diagnostics, identical to the main baseline (0 new)
  • mypy --check-untyped-defs --disable-error-code=import-untyped controllers/inner_api/ — no
    issues (that is the flag set make type-check-core uses; without it you get the usual 205
    pre-existing import-untyped stub warnings)
  • lint-imports — 23 contracts kept, 0 broken
  • dev/lint_response_contracts.py --fail-on-mismatch — 0 mismatch
  • no net-new getattr( in the diff

Behaviour note

As on the earlier PRs in this campaign, invalid bodies now surface as 422
(UnprocessableEntity) from the decorator rather than whatever the handler
previously raised. That is the campaign's intended behaviour.


This PR was written with AI assistance.

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.
Copilot AI lite review requested due to automatic review settings September 1, 2026 06:51
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → 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]

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 61.78% 61.79% +0.01%
Strict coverage 61.38% 61.39% +0.01%
Typed symbols 43,005 43,023 +18
Untyped symbols 26,777 26,776 -1
Modules 3290 3290 0

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 after self.
  • Updated unit tests to stop patching inner_api_ns.payload and instead validate payloads explicitly when calling unwrapped handlers.
  • Added new tests that exercise the real @model_validate behavior (raising UnprocessableEntity for invalid bodies) for each converted site, including EnterpriseMail/BillingMail forwarding via super().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.
@ShousenZHANG

Copy link
Copy Markdown
Contributor Author

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.
API Unit Tests (3.12) went red on
tests/unit_tests/test_config_overrides.py::test_unit_tests_use_validated_config_overrides
— the AST guard that bans direct patch.object(dify_config, ...) in the unit-test tree.
My two new decorator tests were doing exactly that. Fixed in 4df6e14 by switching to
config_overrides_context, matching test_agent_llm.py in the same directory; it also
validates the field names, which the raw patch did not.

The reason I missed it: I ran pytest tests/unit_tests/controllers/inner_api/, and the
guard lives outside that path. The verification list in the description was scoped to the
changed surface and should be read that way — CI is the authority, not that list.

2. "the remaining manual inner_api_ns.payload calls" was not accurate.
controllers/inner_api/workspace/workspace.py has three more (L58, L101, L140). They are
covered by #41540, which I should have referenced. The description now spells out
everything left in inner_api after this PR.

3. My exclusion list got two things wrong.

  • app/dsl.py:110 does not "raise a custom error" — it returns
    {"code": "invalid_workflow_id", ...}, 400. It is also a request.args GET site, not the
    ns.payload shape, so it was never in scope for this PR to begin with.
  • "asserted in their tests" does not hold for agent/files.py. agent/{llm,tools}.py and
    knowledge/retrieval.py do have tests on their except ValidationError branches, but
    test_agent_files.py has none — its only validation test calls model_validate directly
    and never reaches the handler. The reason to leave files.py alone stands regardless (it
    raises AgentFileRequestHttpError(..., status_code=400), which the decorator's 422 would
    change), but the test claim was wrong.

Sorry for the noise. The controller change itself is unchanged from the first push.

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.

2 participants