Custom Tool Authentication#
Custom tools in Dify (called ApiTool) support three API key authentication modes, all handled in assembling_request() before each HTTP call:
auth_type value | Behavior |
|---|---|
api_key / api_key_header | Adds an Authorization (or custom) header, optionally prefixed with Bearer or Basic |
api_key_query | Appends the API key as a query parameter; no header added |
custom prefix | Value written to header exactly as-is |
The header name defaults to Authorization but can be overridden via credentials["api_key_header"] . The prefix is controlled by credentials["api_key_header_prefix"] ("bearer", "basic", or "custom") .
Entry point: ApiTool._invoke() → assembling_request() → do_http_request().
Credential Lifecycle#
Credentials live in ToolRuntime.credentials — a plain dict[str, Any] on a Pydantic BaseModel. A fresh ToolRuntime with decrypted credentials is constructed by ToolManager.get_tool_runtime() and passed into ApiTool.fork_tool_runtime().
The fork_tool_runtime() override deep-copies api_bundle via model_copy() but passes the runtime object by reference. This means the forked tool and its parent share the same ToolRuntime — and crucially, the same credentials dict.
Prefix Accumulation Bug#
Symptom: Repeated invocations of the same custom tool (in agent loops, workflow retries, or parallel branches) produce escalating Authorization header values:
| Call # | Header value sent |
|---|---|
| 1 | Bearer my-token |
| 2 | Bearer Bearer my-token |
| 3 | Bearer Bearer Bearer my-token |
This causes 401/403 failures on every call after the first .
Root cause: Before the fix, assembling_request() wrote the prefixed value back into the shared credentials dict :
# buggy — mutates the shared dict
credentials["api_key_value"] = f"Bearer {credentials['api_key_value']}"
Because credentials is a mutable dict passed by reference through fork_tool_runtime(), and ToolRuntime instances can be reused across invocations, each call found an already-prefixed value and prepended again .
Fix (PR #38616, merged): Store the original api_key_value in a local variable, apply the prefix transformation to the local variable, and use it for the header — leaving credentials unchanged :
# correct — local variable, no mutation
api_key_value = credentials["api_key_value"]
if api_key_header_prefix == "basic" and api_key_value:
api_key_value = f"Basic {api_key_value}"
elif api_key_header_prefix == "bearer" and api_key_value:
api_key_value = f"Bearer {api_key_value}"
headers[api_key_header] = api_key_value
Earlier parallel fixes were also proposed in PR #36350 (with a double-prefix guard) and PR #35997 (local-variable approach) .
Key Files and References#
| Resource | Purpose |
|---|---|
api/core/tools/custom_tool/tool.py | ApiTool class — auth header assembly, HTTP dispatch, fork logic |
api/core/tools/__base/tool_runtime.py | ToolRuntime — holds the credentials dict |
| PR #38616 — fix: fix auth prefix duplicate | Merged fix preventing credential mutation |
| PR #36350 — fix(custom tool): prevent Authorization prefix accumulation | Earlier fix with double-prefix guard |
| PR #35997 — fix: stop auth header prefix accumulating on repeated Custom Tool calls | Parallel fix (local variable only) |
Tests: Regression tests for the fix live in api/tests/unit_tests/core/tools/test_custom_tool.py. They call assembling_request() multiple times sequentially and assert that the credentials["api_key_value"] remains unchanged (e.g. "abc") without accumulating prefixes, while the header value is correctly prefixed on each call.