Chat API returns HTTP 400 invalid_param when sensitive_word_avoidance sent with `enabled: false` + extras
- Dominant language
- TypeScript
- Stars
- 156k
- Forks
- 24.6k
- Avg merge
- 22h 9m
- Merged PRs (30d)
- 610
Description
### Self Checks
- [x] I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
- [x] This is only for bug report, if you would like to ask a question, please head to [Discussions](https://github.com/langgenius/dify/discussions/categories/general).
- [x] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
- [x] I confirm that I am using English to submit this report, otherwise it will be closed.
- [x] 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
- [x] Please do not modify this template :) and fill in all the required fields.
### Dify version
main branch (uncommitted)
### Cloud or Self Hosted
Self Hosted (Source)
### Steps to reproduce
Discovered by peaks-loop code-sweep on 2026-07-02 (related to issue #38327).
1. Spin up Dify self-hosted from `main`.
2. Send a chat completion request with this exact payload body:
```json
{
"inputs": {},
"query": "hello",
"response_mode": "blocking",
"user": "abc",
"sensitive_word_avoidance": {"enabled": false, "type": "", "config": {}}
}
```
3. Observe the response: HTTP 400 with body `{"error": "invalid_param", "message": "Extra inputs are not permitted: type, config"}`.
The trace points at `pydantic.ValidationError` raised inside `core/app/app_config/common/sensitive_word_avoidance/manager.py`.
### ✔️ Expected Behavior
Disabled (`enabled=False`) sensitive-word avoidance configs should silently drop `type` / `config` extra keys so the request flows through without an HTTP 400. The disabled branch should only persist `{enabled: False}` regardless of extra fields the client sent.
### ❌ Actual Behavior
When a chat request carries `{"sensitive_word_avoidance": {"enabled": false, "type": "", "config": {}}}`, the existing `_normalize_raw()` in `core/app/app_config/common/sensitive_word_avoidance/manager.py` falls through every branch: `enabled` is not None, `enabled` is not True, so the leftover `type` / `config` fields are passed straight through to the discriminator-tagged union.
`SensitiveWordAvoidanceDisabledConfig` is declared with `extra="forbid"`, so pydantic raises `ValidationError("Extra inputs are not permitted: type, config")` and the API returns HTTP 400 `invalid_param` — matching the trace in issue #38327.
**Proposed change:** Extend `_normalize_raw()` in `api/core/app/app_config/common/sensitive_word_avoidance/manager.py` to drop every key other than `enabled` when `enabled is False`. Add two parametrized cases to `api/tests/unit_tests/core/app/app_config/common/test_sensitive_word_avoidance_manager.py`:
- `test_validate_disables_when_enabled_false_or_missing` covering the regression (empty leftover fields and non-empty leftover fields).
**Risk:** The disabled case already returns `None` from `convert()` and only persists `{enabled: False}` from `validate_and_set_defaults({...}, mode_dump())`, so the cleanup only affects the input-normalization boundary, not the persisted shape. The `enabled is True` branch and the `enabled is None` branch are untouched. `ModerationFactory.validate_config` is still called for each enabled config, unchanged. No schema, migration, controller, or frontend changes.
Related: issue #38327 (no linked PR).
Contributor guide
Research direction
Start in api/core/app/app_config/common/sensitive_word_avoidance/manager.py by reading _normalize_raw(), then run api/tests/unit_tests/core/app/app_config/common/test_sensitive_word_avoidance_manager.py. Add coverage for disabled configurations with empty and non-empty extra fields; done means both cases avoid HTTP 400 while enabled and missing-enabled behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100