Custom tool sends a form-encoded body when the request body content type is application/json; charset=utf-8
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 68/100
Research direction
Start with api/core/tools/custom_tool/tool.py, especially ApiTool.do_http_request, which the issue identifies as selecting JSON serialization based on an exact Content-Type match. Check for existing tests covering custom tool request bodies, then add coverage for a charset parameter and mixed-case media type. Done when those requests send a JSON body and the existing application/json case still works.
Written by the indexing model from the issue text.
Description
Self Checks
- I have read the Contributing Guide and Language Policy.
- This is only for bug report, if you would like to ask a question, please head to Discussions.
- I have searched for existing issues search for existing issues, including closed ones.
- I confirm that I am using English to submit this report, otherwise it will be closed.
- 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
- Please do not modify this template :) and fill in all the required fields.
Dify version
1.18.0, and main at 60429fd39f (api/core/tools/custom_tool/tool.py is identical in both)
Cloud or Self Hosted
Self Hosted (Source)
Steps to reproduce
A custom tool sends its request body as JSON only when the OpenAPI spec declares the content type as exactly application/json. With application/json; charset=utf-8, the body goes out form-encoded under a JSON Content-Type header.
Schema:
openapi: 3.0.0
info:
title: Items
version: "1.0"
servers:
- url: https://api.example.com
paths:
/items:
post:
operationId: create_item
summary: Create an item
requestBody:
content:
"application/json; charset=utf-8":
schema:
type: object
properties:
name:
type: string
count:
type: integer
To reproduce without a real server, I parsed this schema with ApiBasedToolSchemaParser.auto_parse_to_tool_bundle, built an ApiTool from the bundle and invoked it with name=widget and count=2. The request went through ssrf_proxy to an httpx MockTransport that prints what it receives. The same run with the content type set to plain application/json is shown first for comparison:
--- requestBody content type: application/json
server got Content-Type: application/json
server got body: b'{"name": "widget", "count": 2}'
--- requestBody content type: application/json; charset=utf-8
server got Content-Type: application/json; charset=utf-8
server got body: b'name=widget&count=2'
Script (run from api/ with PYTHONPATH=.)
import httpx
from core.helper import ssrf_proxy
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.custom_tool.tool import ApiTool
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolEntity, ToolIdentity
from core.tools.utils.parser import ApiBasedToolSchemaParser
SPEC = """\
openapi: 3.0.0
info:
title: Items
version: "1.0"
servers:
- url: https://api.example.com
paths:
/items:
post:
operationId: create_item
summary: Create an item
requestBody:
content:
CONTENT_TYPE:
schema:
type: object
properties:
name:
type: string
count:
type: integer
"""
def handler(request: httpx.Request) -> httpx.Response:
print("server got Content-Type:", request.headers.get("content-type"))
print("server got body: ", request.content)
return httpx.Response(200, json={"ok": True})
ssrf_proxy._get_ssrf_client = lambda verify: httpx.Client(transport=httpx.MockTransport(handler))
for content_type in ("application/json", "application/json; charset=utf-8"):
print(f"--- requestBody content type: {content_type}")
bundles, _ = ApiBasedToolSchemaParser.auto_parse_to_tool_bundle(SPEC.replace("CONTENT_TYPE", f'"{content_type}"'))
bundle = bundles[0]
tool = ApiTool(
entity=ToolEntity(identity=ToolIdentity(provider="p", author="a", name="t", label=I18nObject(en_US="t"))),
api_bundle=bundle,
runtime=ToolRuntime(tenant_id="t", credentials={"auth_type": "none"}),
provider_id="p",
)
list(tool._invoke(None, "user", {"name": "widget", "count": 2}))
Expected Behavior
The body is sent as JSON, {"name": "widget", "count": 2}, as it is for application/json. A charset parameter does not change the body format, and media types are case-insensitive, which means Application/JSON should get a JSON body too.
Actual Behavior
The body is sent as name=widget&count=2 with Content-Type: application/json; charset=utf-8. An API that expects JSON cannot parse it.
ApiTool.do_http_request in api/core/tools/custom_tool/tool.py copies the requestBody content type from the spec into the Content-Type header, then calls json.dumps on the body only when that header equals "application/json" exactly. For any other value the body stays a dict and is passed to ssrf_proxy as data, which httpx form-encodes while keeping the JSON header. Application/JSON takes the same path.
I have a fix with a test and will open a PR.
- Dominant language
- Python
- Stars
- 160k
- Forks
- 25.3k
- Avg merge
- 21h 9m
- Merged PRs (30d)
- 614
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from langgenius/dify
-
[Bug] DOCX table cells with manual line breaks still split Markdown tables in knowledge-base extractionPossibly taken @Tong-bit-art claimed this 8 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
langgenius/dify#44498 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
PHP workflow streaming requests buffer the full SSE responsePossibly taken @Winoren claimed this 11 days ago. Openreview: medium
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
langgenius/dify#44270 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
Node.js SDK rejects attachment-only hit testing with an empty queryPossibly taken @Winoren claimed this 11 days ago. Openreview: medium
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
langgenius/dify#44266 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
Node.js SDK rejects empty first-page pagination cursorsPossibly taken @Winoren claimed this 11 days ago. Openreview: medium
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
langgenius/dify#44264 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
CSVExtractor rejects explicit on_bad_lines parsing options with duplicate keywordsPossibly taken @lorenzozanee claimed this 24 days ago. Openreview: medium
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
langgenius/dify#43475 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
Similar issues
-
area/profiles comp/cli P3 type/feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
NousResearch/hermes-agent#146372 ·
Maintainers usually reply within 1 day
-
[BUG] OpenAI-compatible embedding model alias breaks embedder constructionPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
[oblt-aw][autodoc] Document obs-aw-automerge contract and fix automerge doc inconsistenciesPossibly taken @github-actions claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
GeoSeries scale and skew tests fail with pytest 9.2.0Possibly taken @jiayuasu claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
admission-deferred priority: medium status: ready work-class: scoped
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
melodic-software/claude-code-plugins#10887 · 4 comments ·
Maintainers usually reply within 1 day