Clarification on Proxy Configuration for External Network Access in Plugins #15752

Closed
opened 2026-02-21 19:23:11 -05:00 by yindo · 1 comment
Owner

Originally created by @Rivennnnnnnnn on GitHub (Jul 29, 2025).

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.4.3

Cloud or Self Hosted

Self Hosted (Docker)

Steps to reproduce

The plugin I developed is designed to concurrently retrieve knowledge from Dify's knowledge base. During debugging, the plugin works normally, but after being packaged and uploaded to Dify, it fails to connect to any network whatsoever. Requests can only be sent successfully when routed through a proxy. What could be the reason for this? In the official example code, I noticed that no proxy settings are required.

import ast
import asyncio
import json
import logging
import os
from collections.abc import Generator
from typing import Any, Optional, Dict

import httpx
from dify_plugin import Tool
from dify_plugin.entities.tool import ToolInvokeMessage

import gevent
import gevent.monkey
gevent.monkey.patch_all()

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

class KnowledgeQueryTool(Tool):
    async def query_async(self, client: httpx.AsyncClient, question: str,
                          DIFY_URL=None, DATASET_ID=None, BEARER_TOKEN=None) -> Optional[Dict[str, Any]]:
        """使用 httpx.AsyncClient 异步查询 Dify API。"""
        URL = f"{DIFY_URL}/datasets/{DATASET_ID}/retrieve"
        HEADERS = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": f"Bearer {BEARER_TOKEN}",
        }
        payload = {
            "query": question,
            "retrieval_model": {
                "search_mode": "semantic_search",
                "reranking_enable": False,
                "top_k": 10,
                "score_threshold_enabled": True,
                "score_threshold": 0.27
            }
        }
        try:
            logger.debug(f"发送异步请求: URL={URL}, question={question}, payload={payload}")
            response = await client.post(URL, headers=HEADERS, json=payload, timeout=60)
            logger.debug(f"响应状态码: {response.status_code}, 内容: {response.text}")
            response.raise_for_status()
            return response.json()
        except httpx.RequestError as e:
            logger.error(f"异步请求失败: {e}")
            return None

    async def main(self, dify_url, dify_app_key, dify_database_id, questions_str):
        logger.info(f"main 函数启动: questions_str={questions_str}")
        try:
            data_list = ast.literal_eval(questions_str)
            if not isinstance(data_list, list):
                raise TypeError("Query parameter is not a list after evaluation.")
            logger.debug(f"解析问题列表: {data_list}")
        except (ValueError, SyntaxError, TypeError) as e:
            logger.error(f"无法解析问题列表: {questions_str}. 错误: {e}")
            return [f"Error: Invalid query format. Got: {questions_str}"]

        # 代理逻辑:如果 DIFY_URL 是外部,添加 SSRF Proxy
        mounts = None
        if "localhost" not in dify_url and "api" not in dify_url.lower():  # 假设内部 URL 包含这些
            proxy_url = "http://ssrf_proxy:3128"
            mounts = {
                "http://": httpx.AsyncHTTPTransport(proxy=proxy_url),
                "https://": httpx.AsyncHTTPTransport(proxy=proxy_url)
            }
            logger.debug("DIFY_URL 是外部,使用 SSRF Proxy")

        async with httpx.AsyncClient(mounts=mounts) as client:  # 使用 mounts 配置代理
            tasks = [self.query_async(client, q, dify_url, dify_database_id, dify_app_key) for q in data_list]
            results = await asyncio.gather(*tasks, return_exceptions=True)

        contents = []
        for question, result in zip(data_list, results):
            if isinstance(result, Exception):
                logger.error(f"问题 '{question}' 的异步请求失败: {result}")
                contents.append(f"Error processing question '{question}': {str(result)}")
            elif result is not None and 'records' in result:
                records = result.get('records', [])
                if records:
                    for data in records:
                        contents.append(data['segment']['content'])
                    logger.debug(f"问题 '{question}' 处理成功: {len(records)} 条记录")
                else:
                    logger.warning(f"问题 '{question}' 返回空记录,完整响应: {result}")
                    contents.append(f"No records found for '{question}'. Full response: {json.dumps(result)}")
            else:
                logger.warning(f"问题 '{question}' 没有返回有效记录(result={result})")
                contents.append(f"No valid records for '{question}'. Response: {json.dumps(result) if result else 'None'}")

        logger.info(f"main 函数结束: contents={contents}")
        return contents

    def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
        questions_str = tool_parameters["query"]
        dify_url = tool_parameters["dify_api_url"]
        dify_app_key = tool_parameters["dify_knowledge_key"]
        dify_database_id = tool_parameters["dify_knowledge_id"]

        logger.info(f"_invoke 调用: parameters={tool_parameters}")
        contents = asyncio.run(self.main(dify_url, dify_app_key, dify_database_id, questions_str))

        if not contents:
            logger.warning("contents 为空,返回默认消息")
            yield self.create_json_message({
                "result": "No content returned. Check if dataset or query is valid."
            })
        else:
            yield self.create_json_message({
                "result": str(contents),
            })
        logger.info("_invoke 结束")

✔️ Expected Behavior

plugin runs successfully without using a proxy.

Actual Behavior

plugin can not send requests without using a proxy.

Originally created by @Rivennnnnnnnn on GitHub (Jul 29, 2025). ### 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 1.4.3 ### Cloud or Self Hosted Self Hosted (Docker) ### Steps to reproduce The plugin I developed is designed to concurrently retrieve knowledge from Dify's knowledge base. During debugging, the plugin works normally, but after being packaged and uploaded to Dify, it fails to connect to any network whatsoever. Requests can only be sent successfully when routed through a proxy. What could be the reason for this? In the official example code, I noticed that no proxy settings are required. ```python import ast import asyncio import json import logging import os from collections.abc import Generator from typing import Any, Optional, Dict import httpx from dify_plugin import Tool from dify_plugin.entities.tool import ToolInvokeMessage import gevent import gevent.monkey gevent.monkey.patch_all() logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) class KnowledgeQueryTool(Tool): async def query_async(self, client: httpx.AsyncClient, question: str, DIFY_URL=None, DATASET_ID=None, BEARER_TOKEN=None) -> Optional[Dict[str, Any]]: """使用 httpx.AsyncClient 异步查询 Dify API。""" URL = f"{DIFY_URL}/datasets/{DATASET_ID}/retrieve" HEADERS = { "Content-Type": "application/json", "Accept": "application/json", "Authorization": f"Bearer {BEARER_TOKEN}", } payload = { "query": question, "retrieval_model": { "search_mode": "semantic_search", "reranking_enable": False, "top_k": 10, "score_threshold_enabled": True, "score_threshold": 0.27 } } try: logger.debug(f"发送异步请求: URL={URL}, question={question}, payload={payload}") response = await client.post(URL, headers=HEADERS, json=payload, timeout=60) logger.debug(f"响应状态码: {response.status_code}, 内容: {response.text}") response.raise_for_status() return response.json() except httpx.RequestError as e: logger.error(f"异步请求失败: {e}") return None async def main(self, dify_url, dify_app_key, dify_database_id, questions_str): logger.info(f"main 函数启动: questions_str={questions_str}") try: data_list = ast.literal_eval(questions_str) if not isinstance(data_list, list): raise TypeError("Query parameter is not a list after evaluation.") logger.debug(f"解析问题列表: {data_list}") except (ValueError, SyntaxError, TypeError) as e: logger.error(f"无法解析问题列表: {questions_str}. 错误: {e}") return [f"Error: Invalid query format. Got: {questions_str}"] # 代理逻辑:如果 DIFY_URL 是外部,添加 SSRF Proxy mounts = None if "localhost" not in dify_url and "api" not in dify_url.lower(): # 假设内部 URL 包含这些 proxy_url = "http://ssrf_proxy:3128" mounts = { "http://": httpx.AsyncHTTPTransport(proxy=proxy_url), "https://": httpx.AsyncHTTPTransport(proxy=proxy_url) } logger.debug("DIFY_URL 是外部,使用 SSRF Proxy") async with httpx.AsyncClient(mounts=mounts) as client: # 使用 mounts 配置代理 tasks = [self.query_async(client, q, dify_url, dify_database_id, dify_app_key) for q in data_list] results = await asyncio.gather(*tasks, return_exceptions=True) contents = [] for question, result in zip(data_list, results): if isinstance(result, Exception): logger.error(f"问题 '{question}' 的异步请求失败: {result}") contents.append(f"Error processing question '{question}': {str(result)}") elif result is not None and 'records' in result: records = result.get('records', []) if records: for data in records: contents.append(data['segment']['content']) logger.debug(f"问题 '{question}' 处理成功: {len(records)} 条记录") else: logger.warning(f"问题 '{question}' 返回空记录,完整响应: {result}") contents.append(f"No records found for '{question}'. Full response: {json.dumps(result)}") else: logger.warning(f"问题 '{question}' 没有返回有效记录(result={result})") contents.append(f"No valid records for '{question}'. Response: {json.dumps(result) if result else 'None'}") logger.info(f"main 函数结束: contents={contents}") return contents def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: questions_str = tool_parameters["query"] dify_url = tool_parameters["dify_api_url"] dify_app_key = tool_parameters["dify_knowledge_key"] dify_database_id = tool_parameters["dify_knowledge_id"] logger.info(f"_invoke 调用: parameters={tool_parameters}") contents = asyncio.run(self.main(dify_url, dify_app_key, dify_database_id, questions_str)) if not contents: logger.warning("contents 为空,返回默认消息") yield self.create_json_message({ "result": "No content returned. Check if dataset or query is valid." }) else: yield self.create_json_message({ "result": str(contents), }) logger.info("_invoke 结束") ``` ### ✔️ Expected Behavior plugin runs successfully without using a proxy. ### ❌ Actual Behavior plugin can not send requests without using a proxy.
yindo closed this issue 2026-02-21 19:23:11 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 29, 2025):

Hi @Rivennnnnnnnn! I'm Dosu and I’m helping the dify team.

The reason your plugin can't access the network without a proxy after being packaged and uploaded to Dify is due to Docker network isolation and security policies in self-hosted Dify deployments. In these environments, plugin containers typically do not have direct outbound internet access; instead, all external requests must go through the SSRF proxy service (usually at http://ssrf_proxy:3128) defined in the Docker Compose setup. This is a security measure to prevent SSRF and similar attacks, and is enforced by Docker network policies and internal DNS/gateway settings. That's why your plugin works during local debugging (where it has unrestricted network access), but fails without a proxy when running inside the Docker container source.

Official Dify plugin examples don't include explicit proxy logic because they assume the container has direct outbound access or that the deployment environment is configured to allow it. In restricted environments like yours, you must either set the HTTP_PROXY and HTTPS_PROXY environment variables (in uppercase) in the plugin_daemon container, or configure your plugin to use the SSRF proxy for outbound requests source.

Your approach of adding proxy logic in code is a workaround for these network restrictions, but the recommended practice is to handle proxy configuration at the environment or Docker Compose level. If you change proxy settings, make sure to run a full docker-compose down and up -d to apply changes. Also, if you set global proxy variables, use the NO_PROXY variable to exclude internal service names to avoid breaking internal communication source.

In summary: this is expected behavior for secure Docker deployments. Either keep your code's proxy logic for external URLs, or configure the environment so all outbound plugin traffic is routed through the SSRF proxy. Official plugin code doesn't handle this in code because it's meant to be managed by the deployment environment.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Jul 29, 2025): <!-- Greeting --> Hi @Rivennnnnnnnn! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the dify team. <!-- Answer --> The reason your plugin can't access the network without a proxy after being packaged and uploaded to Dify is due to Docker network isolation and security policies in self-hosted Dify deployments. In these environments, plugin containers typically do not have direct outbound internet access; instead, all external requests must go through the SSRF proxy service (usually at `http://ssrf_proxy:3128`) defined in the Docker Compose setup. This is a security measure to prevent SSRF and similar attacks, and is enforced by Docker network policies and internal DNS/gateway settings. That's why your plugin works during local debugging (where it has unrestricted network access), but fails without a proxy when running inside the Docker container [source](https://github.com/langgenius/dify/issues/16548). Official Dify plugin examples don't include explicit proxy logic because they assume the container has direct outbound access or that the deployment environment is configured to allow it. In restricted environments like yours, you must either set the `HTTP_PROXY` and `HTTPS_PROXY` environment variables (in uppercase) in the `plugin_daemon` container, or configure your plugin to use the SSRF proxy for outbound requests [source](https://github.com/langgenius/dify/discussions/21689). Your approach of adding proxy logic in code is a workaround for these network restrictions, but the recommended practice is to handle proxy configuration at the environment or Docker Compose level. If you change proxy settings, make sure to run a full `docker-compose down` and `up -d` to apply changes. Also, if you set global proxy variables, use the `NO_PROXY` variable to exclude internal service names to avoid breaking internal communication [source](https://github.com/langgenius/dify/issues/14608). In summary: this is expected behavior for secure Docker deployments. Either keep your code's proxy logic for external URLs, or configure the environment so all outbound plugin traffic is routed through the SSRF proxy. Official plugin code doesn't handle this in code because it's meant to be managed by the deployment environment. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/192d124c-c3ec-4386-abb4-20df921eeaa4?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/192d124c-c3ec-4386-abb4-20df921eeaa4?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/192d124c-c3ec-4386-abb4-20df921eeaa4?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/192d124c-c3ec-4386-abb4-20df921eeaa4?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/192d124c-c3ec-4386-abb4-20df921eeaa4?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/192d124c-c3ec-4386-abb4-20df921eeaa4?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/192d124c-c3ec-4386-abb4-20df921eeaa4?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/6088a4f4-073a-43f3-b3d4-4f06fb0c05a0/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/langgenius/dify/issues/23128)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify#15752