Skip to content

fix(notify): 自定义 Webhook 推送本地/内网目标绕过代理 - #446

Open
ArmedHelicopter wants to merge 2 commits into
AUTO-MAS-Project:devfrom
ArmedHelicopter:fix/webhook-bypass-local-proxy
Open

fix(notify): 自定义 Webhook 推送本地/内网目标绕过代理#446
ArmedHelicopter wants to merge 2 commits into
AUTO-MAS-Project:devfrom
ArmedHelicopter:fix/webhook-bypass-local-proxy

Conversation

@ArmedHelicopter

@ArmedHelicopter ArmedHelicopter commented Aug 29, 2026

Copy link
Copy Markdown

问题描述

当自定义 Webhook 通知的目标是本机或内网服务(例如本机的消息推送桥接服务)时,如果运行环境中存在代理环境变量(HTTP_PROXY / HTTPS_PROXY 等,使用 Clash 等系统代理的场景下较常见),推送请求会被代理劫持转发。

代理无法连接目标时会返回空响应体HTTP/1.1 502 Bad Gateway,日志与前端表现为:

推送代理结果时出现异常: HTTP 502: 

由于状态码 502 且响应体为空,用户很难判断到底是推送目标本身不可达,还是请求被代理截走了,排查成本较高。本人实际遇到了这个问题:本机推送接收端未运行,本应报"连接被拒绝",实际却收到了来自代理的 502。

根因分析

Notification.WebhookPush 通过 httpx.AsyncClient(proxy=Config.proxy, timeout=10) 发送请求。httpx 默认 trust_env=True——即使显式传入 proxy=None,仍会读取进程环境变量中的代理设置,导致发往 localhost / 内网地址的请求经由系统代理转发。

修复方案

新增 _webhook_client_kwargs(url) 辅助函数,按目标地址区分处理:

  • 本地/内网地址(loopback、localhost 主机名、RFC1918 私网等):trust_env=False 且不使用代理,直连目标;
  • 外部地址:保持原有行为(proxy=Config.proxy + 默认 trust_env),完全向后兼容。

这与代码库中既有惯例一致——app/tools/skland.pyapp/tools/kuro.pyapp/tools/miyoushe.pyapp/tools/taygedo.py 等模块已对特定请求使用 trust_env=False

另外,推送失败的异常信息现在会附带 Webhook 名称(如 [HermesQQ推送] HTTP 502: ...),方便配置了多个通知渠道时快速定位是哪个渠道出了问题。

自测情况

  • 启动本地 HTTP 服务,并设置指向不可达端口的 HTTP_PROXY 环境变量:修复后本地 Webhook 推送直连成功;修复前同场景得到的是代理返回的 502
  • 外部地址(如 Server酱)仍携带 Config.proxy,行为与修复前一致
  • 目标为 localhost 主机名、127.0.0.1、内网 IP 时均正确判定为本地直连
  • python -m py_compile 通过

以上是我在排查自己环境问题时定位到的根因与修复,已在本机稳定运行。如有考虑不周之处,恳请各位开发者指正,我会及时跟进调整。感谢团队维护这么棒的开源项目!

Sourcery 摘要

使自定义 Webhook 传送能够直接访问本地和私有网络目标,同时保留对外部目标的代理支持。

错误修复:

  • 确保发往 localhost 和私有网络目标的自定义 Webhook 请求绕过已配置的代理和环境代理,以避免代理生成的误导性失败信息。
  • 在传送失败的错误消息中包含 Webhook 名称,以便更轻松地识别受影响的通知渠道。

增强功能:

  • 保留外部 Webhook 目标的代理行为,同时对本地和私有网络目标使用直接连接。
Original summary in English

Summary by Sourcery

Make custom Webhook delivery route local and private-network targets directly while retaining proxy support for external destinations.

Bug Fixes:

  • Ensure custom Webhook requests to localhost and private-network targets bypass configured and environment proxies to avoid misleading proxy-generated failures.
  • Include the Webhook name in failed delivery error messages to simplify identifying the affected notification channel.

Enhancements:

  • Preserve proxy behavior for external Webhook destinations while applying direct connections to local and private targets.

DLmaster361 and others added 2 commits August 26, 2026 16:42
httpx 默认 trust_env=True 会读取环境变量中的代理设置,导致发往
localhost/内网的自定义 Webhook 推送被系统代理(如 Clash)劫持;
代理拨号失败时返回空响应体的 502 Bad Gateway,难以定位真实故障。

新增 _webhook_client_kwargs 辅助函数:目标为 loopback/内网地址时
trust_env=False 且不走代理直连;外部目标保持原有 proxy=Config.proxy
行为不变,完全向后兼容。与 app/tools 下 skland、kuro、miyoushe、
taygedo 等模块对特定请求使用 trust_env=False 的既有惯例一致。

同时让推送失败的异常信息附带 Webhook 名称,便于配置多个通知渠道时定位问题。
@sourcery-ai

sourcery-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

审查者指南

通过按目标地址动态配置 httpx 代理参数,修复本地/内网 Webhook 被环境代理劫持并返回误导性 502 的问题,同时保持外部 Webhook 的原有代理兼容性,并在失败信息中补充 Webhook 名称。

本地和外部 Webhook 代理选择流程图

flowchart TD
    A[Webhook push] --> B["_webhook_client_kwargs(url)"]
    B --> C{Local or private target?}
    C -->|Yes| D["AsyncClient(timeout=10, trust_env=False)"]
    C -->|No| E["AsyncClient(timeout=10, proxy=Config.proxy)"]
    D --> F[client.post or client.get]
    E --> F
    F --> G{status_code == 200}
    G -->|Yes| H[Log successful Webhook push]
    G -->|No| I[raise Exception with Webhook name and HTTP status]
Loading

文件级变更

变更 详情 文件
根据 Webhook 目标地址选择代理策略,使本地和内网请求绕过代理,同时保留外部请求的既有代理行为。
  • 解析 URL 主机名并识别 loopback、localhost 和私有 IP 地址。
  • 本地/内网目标使用 trust_env=False 直连;外部目标继续使用 Config.proxy 并读取环境代理。
  • 统一通过辅助函数构造 httpx.AsyncClient 参数。
app/services/notification.py
增强自定义 Webhook 推送失败信息的可定位性。
  • 在异常中加入 Webhook 名称,便于区分多个通知渠道的失败来源。
app/services/notification.py

提示和命令

与 Sourcery 互动

  • 触发新的审查: 在拉取请求中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 根据审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以回复审查评论并使用 @sourcery-ai issue,根据该评论创建 issue。
  • 生成拉取请求标题: 在拉取请求标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title,随时(重新)生成标题。
  • 生成拉取请求摘要: 在拉取请求正文的任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary,随时(重新)生成摘要。
  • 生成审查者指南: 在拉取请求中评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理了所有评论并不想再看到它们,这项功能会很有用。
  • 忽略所有 Sourcery 审查: 在拉取请求中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。如果你想通过新的审查重新开始,这项功能尤其有用——别忘了评论 @sourcery-ai review 以触发新的审查!

自定义你的使用体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

通过按目标地址动态配置 httpx 代理参数,修复本地/内网 Webhook 被环境代理劫持并返回误导性 502 的问题,同时保持外部 Webhook 的原有代理兼容性,并在失败信息中补充 Webhook 名称。

Flow diagram for local and external Webhook proxy selection

flowchart TD
    A[Webhook push] --> B["_webhook_client_kwargs(url)"]
    B --> C{Local or private target?}
    C -->|Yes| D["AsyncClient(timeout=10, trust_env=False)"]
    C -->|No| E["AsyncClient(timeout=10, proxy=Config.proxy)"]
    D --> F[client.post or client.get]
    E --> F
    F --> G{status_code == 200}
    G -->|Yes| H[Log successful Webhook push]
    G -->|No| I[raise Exception with Webhook name and HTTP status]
Loading

File-Level Changes

Change Details Files
根据 Webhook 目标地址选择代理策略,使本地和内网请求绕过代理,同时保留外部请求的既有代理行为。
  • 解析 URL 主机名并识别 loopback、localhost 和私有 IP 地址。
  • 本地/内网目标使用 trust_env=False 直连;外部目标继续使用 Config.proxy 并读取环境代理。
  • 统一通过辅助函数构造 httpx.AsyncClient 参数。
app/services/notification.py
增强自定义 Webhook 推送失败信息的可定位性。
  • 在异常中加入 Webhook 名称,便于区分多个通知渠道的失败来源。
app/services/notification.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

你好——我发现了 1 个问题

AI Agent 提示词
请处理本次代码审查中的评论:

## 单独评论

### 评论 1
<location path="app/services/notification.py" line_range="311-319" />
<code_context>
-        async with httpx.AsyncClient(proxy=Config.proxy, timeout=10) as client:
+        url = webhook.get("Data", "Url")
+
+        async with httpx.AsyncClient(**_webhook_client_kwargs(url)) as client:
             if webhook.get("Data", "Method") == "POST":
                 if isinstance(data, dict):
</code_context>
<issue_to_address>
**issue (bug_risk):** 网络连接失败、超时或 DNS 解析失败时,`client.post()`/`client.get()` 直接抛出 httpx 异常,不会附带 Webhook 名称;因此多个渠道配置下,最需要定位的本地目标连接拒绝错误仍无法判断来自哪个 Webhook。

**Triggers:** 当 Webhook 目标不可达、连接被拒绝或请求超时时。

**Suggested fix:** 捕获 httpx 的请求异常并在重新抛出时加入 `webhook.get('Info', 'Name')`,同时保留原异常作为 cause。
</issue_to_address>

Sourcery 评估

需要人工审查。 请先处理 1 个待解决的问题;此外,此更改通过禁用已配置的代理和环境代理设置,改变了所有 localhost 或私有 IP Webhook 的网络信任边界。如果该策略不正确,Webhook 负载可能会被直接发送到非预期的内部目标,或绕过必要的监控/控制路径;回滚可以阻止后续请求,但无法撤回已经传输的数据。

阻塞性问题:app/services/notification.py:319


Sourcery 对开源项目免费——如果你喜欢我们的审查结果,请考虑分享给他人 ✨
请帮助我变得更有用!请对每条评论点击 👍 或 👎,我会利用这些反馈来改进审查结果。
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/services/notification.py" line_range="311-319" />
<code_context>
-        async with httpx.AsyncClient(proxy=Config.proxy, timeout=10) as client:
+        url = webhook.get("Data", "Url")
+
+        async with httpx.AsyncClient(**_webhook_client_kwargs(url)) as client:
             if webhook.get("Data", "Method") == "POST":
                 if isinstance(data, dict):
</code_context>
<issue_to_address>
**issue (bug_risk):** 网络连接失败、超时或 DNS 解析失败时,`client.post()`/`client.get()` 直接抛出 httpx 异常,不会附带 Webhook 名称;因此多个渠道配置下,最需要定位的本地目标连接拒绝错误仍无法判断来自哪个 Webhook。

**Triggers:** 当 Webhook 目标不可达、连接被拒绝或请求超时时。

**Suggested fix:** 捕获 httpx 的请求异常并在重新抛出时加入 `webhook.get('Info', 'Name')`,同时保留原异常作为 cause。
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and this changes the network trust boundary for every localhost or private-IP webhook by disabling the configured proxy and environment proxy settings. If that policy is wrong, webhook payloads can be sent directly to an unintended internal destination or bypass a required monitoring/control path; reverting stops future requests but cannot undo data already transmitted.

Blocking findings: app/services/notification.py:319


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +311 to 319
async with httpx.AsyncClient(**_webhook_client_kwargs(url)) as client:
if webhook.get("Data", "Method") == "POST":
if isinstance(data, dict):
response = await client.post(
url=webhook.get("Data", "Url"), json=data, headers=headers
)
response = await client.post(url=url, json=data, headers=headers)
elif isinstance(data, str):
response = await client.post(
url=webhook.get("Data", "Url"), content=data, headers=headers
)
response = await client.post(url=url, content=data, headers=headers)
elif webhook.get("Data", "Method") == "GET":
if isinstance(data, dict):
# Flatten params to ensure all values are str or list of str

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 网络连接失败、超时或 DNS 解析失败时,client.post()/client.get() 直接抛出 httpx 异常,不会附带 Webhook 名称;因此多个渠道配置下,最需要定位的本地目标连接拒绝错误仍无法判断来自哪个 Webhook。

Triggers: 当 Webhook 目标不可达、连接被拒绝或请求超时时。

Suggested fix: 捕获 httpx 的请求异常并在重新抛出时加入 webhook.get('Info', 'Name'),同时保留原异常作为 cause。

Original comment in English

issue (bug_risk): 网络连接失败、超时或 DNS 解析失败时,client.post()/client.get() 直接抛出 httpx 异常,不会附带 Webhook 名称;因此多个渠道配置下,最需要定位的本地目标连接拒绝错误仍无法判断来自哪个 Webhook。

Triggers: 当 Webhook 目标不可达、连接被拒绝或请求超时时。

Suggested fix: 捕获 httpx 的请求异常并在重新抛出时加入 webhook.get('Info', 'Name'),同时保留原异常作为 cause。

@HarcoChen
HarcoChen changed the base branch from main to dev August 29, 2026 12:50
@HarcoChen

Copy link
Copy Markdown
Contributor

这个问题看着像是你代理软件没有配置好,此外我认为将localhost全部替换为127.0.0.1可以解决本机连接劫持的问题?

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.

3 participants