Skip to content

fix(proxy): 检测 200+HTML 的假200并触发故障转移(#749)#763

Closed
tesgth032 wants to merge 5 commits intoding113:devfrom
tesgth032:fix/fake-200-html-issue-749
Closed

fix(proxy): 检测 200+HTML 的假200并触发故障转移(#749)#763
tesgth032 wants to merge 5 commits intoding113:devfrom
tesgth032:fix/fake-200-html-issue-749

Conversation

@tesgth032
Copy link
Contributor

@tesgth032 tesgth032 commented Feb 11, 2026

Summary

Detects fake HTTP 200 responses that return HTML pages (e.g., Cloudflare/WAF/maintenance pages) instead of valid API responses, and triggers failover/circuit-breaker logic so the proxy correctly retries with another provider.

Problem

Some upstream providers or relay stations return HTTP 200 with text/html content (Cloudflare challenge pages, WAF blocks, maintenance pages) instead of a proper API response. These fake successes cause:

  • Circuit breaker / failover stats incorrectly recording a success
  • Session binding updating to an unavailable provider, causing subsequent requests to keep hitting the bad provider

Related Issues & PRs:

Solution

Two-pronged detection approach, both conservative to avoid false positives:

  1. Non-streaming path (ProxyForwarder): When the upstream response has content-type: text/html or application/xhtml+xml, clone and inspect the body. If it looks like a full HTML document (doctype / <html> tag), throw ProxyError(502) so existing retry/failover/circuit-breaker logic kicks in, and recordSuccess() / session binding are skipped.

  2. Shared detection utility (upstream-error-detection.ts): Added FAKE_200_HTML_BODY pattern to detectUpstreamErrorFromSseOrJsonText(), reusing the same fake-200 detection pipeline for consistency.

Changes

Core Changes

  • src/app/v1/_lib/proxy/forwarder.ts (+29/-3) - Added HTML content-type check in the non-streaming success branch; clones response body for sniffing and throws ProxyError(502) on HTML document detection
  • src/lib/utils/upstream-error-detection.ts (+26/-0) - Added FAKE_200_HTML_BODY error pattern with HTML document regex detection

Tests

  • tests/unit/proxy/proxy-forwarder-fake-200-html.test.ts (+224/-0) - Comprehensive test suite covering HTML fake-200 identification and failover triggering
  • src/lib/utils/upstream-error-detection.test.ts (+25/-0) - Unit tests for the new HTML body detection pattern
  • tests/unit/lib/provider-endpoints/probe.test.ts (+12/-0) - Additional probe test coverage

Breaking Changes

None. This is a purely additive detection enhancement in the proxy pipeline. No API changes, no schema changes, no removed exports.

Testing

Automated Tests

  • Unit tests added for HTML fake-200 detection logic
  • Unit tests added for forwarder failover path on HTML responses
  • Existing upstream-error-detection tests extended

Manual Testing

  1. Configure a provider that returns 200 + HTML (e.g., behind Cloudflare challenge)
  2. Send a request through the proxy
  3. Verify the response triggers failover to the next provider instead of being treated as success
  4. Confirm circuit breaker stats do not record a false success

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • No breaking changes

Description enhanced by Claude AI - original description preserved below


Original Description (Chinese)

背景

Issue #749:部分上游/中转站会返回 HTTP 200,但实际响应为 text/html 的 HTML 页面(常见于 Cloudflare/WAF/维护页)。

这类响应如果被当作成功:

  • 熔断/故障转移统计会被误记为 success;
  • session 智能绑定会被更新到不可用 provider,导致后续重试继续复用坏 provider。

改动

  • ProxyForwarder 的非流式成功分支中:当 content-type 指向 text/html/application/xhtml+xml 时,clone 读取 body 并做 强信号嗅探
    • 若 body 看起来是完整 HTML 文档(doctype/html 标签),则抛出 ProxyError(502),使现有的重试/切换/熔断逻辑正常生效,并避免 recordSuccess() / session 绑定被更新。
  • detectUpstreamErrorFromSseOrJsonText() 中补充 HTML 文档检测(FAKE_200_HTML_BODY),用于复用同一套"假 200"判定逻辑。

说明

  • 该检测偏保守:仅对明显 HTML 文档判错,避免误判正常 JSON/SSE 返回。
  • 仅增强通用 fake-200 检测,未引入"自定义供应商错误检测"。

测试

  • 新增/更新单测覆盖:HTML 假 200 识别 + forwarder 触发故障转移路径。

Fixes #749

Greptile Overview

Greptile Summary

This PR improves proxy failover correctness by detecting “fake 200” upstream responses that are actually HTML documents (e.g. WAF/Cloudflare/maintenance pages) and converting them into a failure signal so existing retry/circuit-breaker/session-binding logic behaves as if the upstream failed.

Key changes:

  • src/app/v1/_lib/proxy/forwarder.ts adds non-streaming body sniffing (capped at 1MiB) for HTML content-types and throws ProxyError(502) when the body looks like a real HTML document.
  • src/lib/utils/upstream-error-detection.ts adds a shared FAKE_200_HTML_BODY detection path (doctype / <html> tag sniffing) and returns a sanitized/truncated detail for logging/diagnostics.
  • Tests were added/updated to cover the new detection and failover behavior.

Main issue to address before merge: at least one tests/unit/lib/provider-endpoints/probe.test.ts case still mocks an incomplete @/lib/endpoint-circuit-breaker module even though probe.ts now imports additional symbols at module scope; this will cause those tests to throw when importing probe.ts. Also, the new forwarder test file appears to be committed with CRLF line endings, which can violate repo formatting expectations and create noisy diffs.

Confidence Score: 4/5

  • Mostly safe to merge once the failing unit test mock issue is fixed.
  • Core proxy logic changes look conservative and covered by targeted unit tests, but the probe test suite has at least one incomplete module mock that will crash on import, and the new test file’s CRLF line endings may break formatting/tooling checks.
  • tests/unit/lib/provider-endpoints/probe.test.ts; tests/unit/proxy/proxy-forwarder-fake-200-html.test.ts

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/forwarder.ts Adds non-streaming response body sniffing (1MiB cap) to detect fake 200 HTML documents and throw ProxyError(502) to trigger failover.
src/lib/utils/upstream-error-detection.ts Adds FAKE_200_HTML_BODY detection via doctype/ sniffing and returns sanitized, truncated detail.
src/lib/utils/upstream-error-detection.test.ts Adds unit tests covering HTML-doc detection and ensuring JSON with '' text is not misclassified.
tests/unit/lib/provider-endpoints/probe.test.ts Updates endpoint-circuit-breaker mocks for new imports, but at least one test still mocks an incomplete module and will crash on import.
tests/unit/proxy/proxy-forwarder-fake-200-html.test.ts Adds forwarder failover tests for 200+HTML and missing_content, but file appears to be CRLF which may fail repo EOL expectations.

Sequence Diagram

sequenceDiagram
  autonumber
  participant Client
  participant ProxyForwarder
  participant UpstreamProvider as Provider
  participant UpstreamDetect as detectUpstreamErrorFromSseOrJsonText
  participant CircuitBreaker
  participant ProviderSelector

  Client->>ProxyForwarder: send(session)
  ProxyForwarder->>UpstreamProvider: doForward(request)
  UpstreamProvider-->>ProxyForwarder: Response(200, content-type)

  alt Streaming (text/event-stream)
    ProxyForwarder->>ProxyForwarder: setDeferredStreamingFinalization(...)
    ProxyForwarder-->>Client: stream response
  else Non-streaming
    ProxyForwarder->>ProxyForwarder: if content-length == 0 throw EmptyResponseError
    alt HTML content-type (text/html or application/xhtml+xml)
      ProxyForwarder->>ProxyForwarder: clone response + readResponseTextUpTo(1MiB)
      ProxyForwarder->>UpstreamDetect: detect(text)
      alt detected FAKE_200_HTML_BODY
        ProxyForwarder->>CircuitBreaker: recordFailure(providerId, ProxyError)
        ProxyForwarder->>ProviderSelector: pickRandomProviderWithExclusion(exclude providerId)
        ProxyForwarder->>UpstreamProvider: doForward(request) on next provider
        UpstreamProvider-->>ProxyForwarder: Response(success)
        ProxyForwarder->>CircuitBreaker: recordSuccess(newProviderId)
        ProxyForwarder-->>Client: response
      else not HTML-doc
        ProxyForwarder->>CircuitBreaker: recordSuccess(providerId)
        ProxyForwarder-->>Client: response
      end
    else Missing content-length
      ProxyForwarder->>ProxyForwarder: clone response + readResponseTextUpTo(1MiB)
      ProxyForwarder->>ProxyForwarder: JSON parse + missing_content checks
      ProxyForwarder->>CircuitBreaker: recordSuccess(providerId)
      ProxyForwarder-->>Client: response
    end
  end
Loading

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @tesgth032, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在解决代理服务中一个关键问题:当上游服务返回 HTTP 200 状态码但实际内容是 HTML 错误页面时,系统会错误地将其识别为成功响应。通过引入对 text/html 内容类型的强信号嗅探机制,本 PR 确保了此类“假 200”响应能够被正确识别为故障,从而保障了熔断、故障转移和会话绑定的准确性,显著提升了代理服务的健壮性和可靠性。

Highlights

  • 假 200 HTML 响应检测: 在 ProxyForwarder 中新增逻辑,用于检测 HTTP 200 响应但实际内容为 text/htmlapplication/xhtml+xml 的 HTML 页面,这些页面通常是上游网关、WAF 或维护页的错误响应。
  • 触发故障转移: 当检测到此类“假 200”HTML 响应时,系统会抛出 ProxyError(502),从而触发现有的重试、切换和熔断逻辑,避免将不可用提供商误记为成功并绑定会话。
  • 错误检测工具增强: 更新了 detectUpstreamErrorFromSseOrJsonText 工具函数,使其能够识别明显的 HTML 文档(通过 doctypehtml 标签),并添加了相应的单元测试以覆盖这些新场景。
Changelog
  • src/app/v1/_lib/proxy/forwarder.ts
    • 导入了 detectUpstreamErrorFromSseOrJsonText 函数。
    • 添加了 normalizedContentTypeisHtml 变量来标准化和检测 HTML 内容类型。
    • 实现了在非流式成功分支中克隆并读取响应体,以便对 HTML 内容进行检测。
    • 如果检测到 HTML 响应为“假 200”,则抛出 ProxyError(502)
    • 调整了空响应体检测逻辑,以利用已克隆的响应文本。
  • src/lib/utils/upstream-error-detection.test.ts
    • 新增了测试用例,用于验证明显的 HTML 文档(包含 doctypehtml 标签)会被正确识别为错误。
    • 新增了测试用例,确保包含 <html> 文本的纯 JSON 不会被误判为 HTML 错误。
  • src/lib/utils/upstream-error-detection.ts
    • 更新了文件注释,明确指出对明显 HTML 文档的强信号判定是设计目标之一。
    • FAKE_200_CODES 中新增了 HTML_BODY 错误码。
    • 引入了 isLikelyHtmlDocument 函数,用于通过检查 doctypehtml 标签来判断文本是否为 HTML 文档。
    • detectUpstreamErrorFromSseOrJsonText 函数中,将 HTML 文档检测逻辑置于 JSON 解析之前。
  • tests/unit/lib/provider-endpoints/probe.test.ts
    • 在多个测试设置中,为 @/lib/endpoint-circuit-breaker 模块添加了 getEndpointCircuitStateSyncresetEndpointCircuit 的模拟实现。
  • tests/unit/proxy/proxy-forwarder-fake-200-html.test.ts
    • 新增了测试文件,专门用于测试 ProxyForwarder 中“假 200 HTML 响应体”的检测和故障转移逻辑。
Activity
  • 此拉取请求引入了新的功能,以处理包含 HTML 内容的“假 200”响应。
  • 它更新了核心代理转发逻辑和错误检测工具。
  • 新增了单元测试,以验证 HTML 假 200 检测并确保正确的故障转移。
  • 现有提供商端点单元测试已更新,增加了额外的模拟。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

在 forwarder 中增加对非流式 200 响应体的检测与截断读取(含 HTML 嗅探),在识别为伪 200(FAKE_200_HTML_BODY 或 上游 JSON 异常)时抛出 ProxyError 并记录/触发提供商切换;新增上游错误检测工具与单元测试,并调整若干测试桩。

Changes

Cohort / File(s) Summary
代理转发器核心
src/app/v1/_lib/proxy/forwarder.ts
新增非流式响应体安全读取(NON_STREAM_BODY_INSPECTION_MAX_BYTES、readResponseTextUpTo)、Content-Type 归一化与 isHtml 检测;在 200 响应上优先执行 HTML/JSON 上游错误检测(包括 FAKE_200_HTML_BODY),在检测到上游错误时抛出 ProxyError 并触发 provider 故障记录/切换;调整非流式 JSON 解析、截断时跳过解析与相应日志分支。
上游错误检测工具
src/lib/utils/upstream-error-detection.ts, src/lib/utils/upstream-error-detection.test.ts
新增 HTML 文档嗅探(isLikelyHtmlDocument、HTML_DOC_SNIFF_MAX_CHARS、正则等),将 FAKE_200_CODES 扩展为包含 HTML_BODY;在 detectUpstreamErrorFromSseOrJsonText 中优先返回 HTML_BODY 错误;增加测试覆盖 HTML 页面与含 "" 字符串的 JSON 不被误判。
代理转发器单元测试
tests/unit/proxy/proxy-forwarder-fake-200-html.test.ts
新增测试模块:模拟首个提供商返回 200 HTML(识别为伪 200 并记录失败)或 JSON 缺失 content 的情形,然后切换到备用提供商并成功返回 JSON;断言 doForward 调用顺序、提供商选择、recordFailure/recordSuccess 行为。
测试桩/探针测试更新
tests/unit/lib/provider-endpoints/probe.test.ts
在多个测试 mock 中新增 getEndpointCircuitStateSync()(返回 "closed")与 resetEndpointCircuit()(async no-op)以配合电路断路器相关测试场景。
其它(元数据/包清单)
package.json
包清单有相关行变更(包含在本次 diff 统计)。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR标题准确反映了主要改动:检测200+HTML的假200响应并触发故障转移,与changeset相符。
Description check ✅ Passed PR描述详细说明了问题背景、解决方案和改动范围,与changeset相关联且信息完整。
Linked Issues check ✅ Passed PR实现了Issue #749的核心需求:检测HTTP 200+HTML响应并触发故障转移,防止虚假成功记录和会话绑定。
Out of Scope Changes check ✅ Passed 所有改动都围绕HTML假200检测的核心目标,未涉及超出范围的自定义供应商错误检测功能。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added bug Something isn't working area:core labels Feb 11, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 678 to 681
if (isHtml || !contentLength) {
const clonedResponse = response.clone();
clonedResponseText = await clonedResponse.text();
}
Copy link

Choose a reason for hiding this comment

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

potential double clone when !contentLength && isHtml both true - body read twice unnecessarily

Suggested change
if (isHtml || !contentLength) {
const clonedResponse = response.clone();
clonedResponseText = await clonedResponse.text();
}
let clonedResponseText: string | undefined;
if (isHtml || !contentLength) {
const clonedResponse = response.clone();
clonedResponseText = await clonedResponse.text();
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 678:681

Comment:
potential double clone when `!contentLength && isHtml` both true - body read twice unnecessarily

```suggestion
            let clonedResponseText: string | undefined;
            if (isHtml || !contentLength) {
              const clonedResponse = response.clone();
              clonedResponseText = await clonedResponse.text();
            }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR addresses issues with failover and session binding when upstream services return "fake 200" HTML error pages. While the implementation is clear, efficient, and well-tested, it introduces a potential Denial of Service (DoS) vulnerability. The proxy buffers the entire response body in memory without a size limit when detecting text/html content or if Content-Length is missing. This could lead to memory exhaustion (OOM) or block the event loop if a malicious upstream sends large responses. It is recommended to implement a strict size limit (e.g., 1MB) for response body buffering during error detection. A minor improvement could also be made to the HTML detection regular expression for stricter matching to prevent potential misjudgments.

Comment on lines 678 to 681
if (isHtml || !contentLength) {
const clonedResponse = response.clone();
clonedResponseText = await clonedResponse.text();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The code reads the entire upstream response body into memory using response.text() without enforcing a maximum size limit. This occurs when the response is identified as HTML or when the Content-Length header is missing. A malicious or compromised upstream provider could send an excessively large response, leading to process-wide memory exhaustion (OOM). Furthermore, the resulting large string is passed to detectUpstreamErrorFromSseOrJsonText, which synchronously parses it as JSON if it appears to be a JSON object. Parsing a very large JSON string blocks the Node.js event loop, causing a denial of service for all concurrent requests handled by the proxy.

To remediate this, implement a size limit when reading the response body for error detection. Use a helper function to read the stream up to a fixed limit (e.g., 1MB) and abort if the limit is exceeded.


const HTML_DOC_SNIFF_MAX_CHARS = 1024;
const HTML_DOCTYPE_RE = /^<!doctype\s+html[\s>]/i;
const HTML_HTML_TAG_RE = /<html[\s>]/i;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

当前的正则表达式 /<html[\s>]/i 会匹配首次出现 <html 的任何位置(在前1024个字符内),这可能会导致误判。例如,一个 XML 文档(以 < 开头)如果其内部包含了 <html> 标签,也可能被错误地识别为 HTML 文档。为了使检测更精确,与“偏保守”的设计目标更一致,建议将正则表达式锚定在字符串的开头,只匹配以 <html>(前面可以有空格)开头的文档。

Suggested change
const HTML_HTML_TAG_RE = /<html[\s>]/i;
const HTML_HTML_TAG_RE = /^\s*<html[\s>]/i;

@github-actions github-actions bot added the size/M Medium PR (< 500 lines) label Feb 11, 2026
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR. The implementation is conservative and well-scoped: it detects fake 200 responses with HTML bodies (e.g., Cloudflare/WAF error pages) in the non-streaming path and correctly triggers the existing retry/failover/circuit-breaker machinery by throwing a ProxyError(502). The detection logic uses strong signals only (doctype/html tags), the response clone strategy is sound, and the refactoring to share clonedResponseText between the HTML check and the existing empty-body check avoids redundant clones.

PR Size: M

  • Lines changed: 319 (316 additions, 3 deletions)
  • Files changed: 5

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Good

Automated review by Claude AI

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 735 to 743
if (isHtml || !contentLength) {
const clonedResponse = response.clone();
const inspected = await readResponseTextUpTo(
clonedResponse,
NON_STREAM_BODY_INSPECTION_MAX_BYTES
);
inspectedText = inspected.text;
inspectedTruncated = inspected.truncated;
}
Copy link

Choose a reason for hiding this comment

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

When isHtml is false but !contentLength is true, the body is read into inspectedText but never used for HTML detection (line 745 checks isHtml && inspectedText !== undefined). This means non-HTML responses without Content-Length will have their body read twice unnecessarily - once here and again at line 759 where inspectedText is reused only for the empty check.

Consider optimizing to only read when needed:

Suggested change
if (isHtml || !contentLength) {
const clonedResponse = response.clone();
const inspected = await readResponseTextUpTo(
clonedResponse,
NON_STREAM_BODY_INSPECTION_MAX_BYTES
);
inspectedText = inspected.text;
inspectedTruncated = inspected.truncated;
}
let inspectedText: string | undefined;
let inspectedTruncated = false;
if (!contentLength) {
const clonedResponse = response.clone();
const inspected = await readResponseTextUpTo(
clonedResponse,
NON_STREAM_BODY_INSPECTION_MAX_BYTES
);
inspectedText = inspected.text;
inspectedTruncated = inspected.truncated;
// Perform HTML detection on the already-read body if applicable
if (isHtml) {
const detected = detectUpstreamErrorFromSseOrJsonText(inspectedText);
if (detected.isError && detected.code === "FAKE_200_HTML_BODY") {
throw new ProxyError(detected.code, 502, {
body: detected.detail ?? "",
providerId: currentProvider.id,
providerName: currentProvider.name,
});
}
}
} else if (isHtml) {
// Has Content-Length but is HTML - need to read for detection
const clonedResponse = response.clone();
const inspected = await readResponseTextUpTo(
clonedResponse,
NON_STREAM_BODY_INSPECTION_MAX_BYTES
);
const detected = detectUpstreamErrorFromSseOrJsonText(inspected.text);
if (detected.isError && detected.code === "FAKE_200_HTML_BODY") {
throw new ProxyError(detected.code, 502, {
body: detected.detail ?? "",
providerId: currentProvider.id,
providerName: currentProvider.name,
});
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 735:743

Comment:
When `isHtml` is false but `!contentLength` is true, the body is read into `inspectedText` but never used for HTML detection (line 745 checks `isHtml && inspectedText !== undefined`). This means non-HTML responses without Content-Length will have their body read twice unnecessarily - once here and again at line 759 where `inspectedText` is reused only for the empty check.

Consider optimizing to only read when needed:
```suggestion
            let inspectedText: string | undefined;
            let inspectedTruncated = false;
            if (!contentLength) {
              const clonedResponse = response.clone();
              const inspected = await readResponseTextUpTo(
                clonedResponse,
                NON_STREAM_BODY_INSPECTION_MAX_BYTES
              );
              inspectedText = inspected.text;
              inspectedTruncated = inspected.truncated;
              
              // Perform HTML detection on the already-read body if applicable
              if (isHtml) {
                const detected = detectUpstreamErrorFromSseOrJsonText(inspectedText);
                if (detected.isError && detected.code === "FAKE_200_HTML_BODY") {
                  throw new ProxyError(detected.code, 502, {
                    body: detected.detail ?? "",
                    providerId: currentProvider.id,
                    providerName: currentProvider.name,
                  });
                }
              }
            } else if (isHtml) {
              // Has Content-Length but is HTML - need to read for detection
              const clonedResponse = response.clone();
              const inspected = await readResponseTextUpTo(
                clonedResponse,
                NON_STREAM_BODY_INSPECTION_MAX_BYTES
              );
              const detected = detectUpstreamErrorFromSseOrJsonText(inspected.text);
              if (detected.isError && detected.code === "FAKE_200_HTML_BODY") {
                throw new ProxyError(detected.code, 502, {
                  body: detected.detail ?? "",
                  providerId: currentProvider.id,
                  providerName: currentProvider.name,
                });
              }
            }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +94 to +142
async function readResponseTextUpTo(
response: Response,
maxBytes: number
): Promise<{ text: string; truncated: boolean }> {
const reader = response.body?.getReader();
if (!reader) {
return { text: "", truncated: false };
}

const decoder = new TextDecoder();
const chunks: string[] = [];
let bytesRead = 0;
let truncated = false;

while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!value || value.byteLength === 0) continue;

const remaining = maxBytes - bytesRead;
if (remaining <= 0) {
truncated = true;
break;
}

if (value.byteLength > remaining) {
chunks.push(decoder.decode(value.subarray(0, remaining), { stream: true }));
bytesRead += remaining;
truncated = true;
break;
}

chunks.push(decoder.decode(value, { stream: true }));
bytesRead += value.byteLength;
}

const flushed = decoder.decode();
if (flushed) chunks.push(flushed);

if (truncated) {
try {
await reader.cancel();
} catch {
// ignore
}
}

return { text: chunks.join(""), truncated };
}
Copy link

Choose a reason for hiding this comment

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

Added readResponseTextUpTo function to safely read response bodies with a 1MB limit, preventing memory exhaustion from malicious or misconfigured upstream providers.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 94:142

Comment:
Added `readResponseTextUpTo` function to safely read response bodies with a 1MB limit, preventing memory exhaustion from malicious or misconfigured upstream providers.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 779-831: The EmptyResponseError thrown inside the JSON parsing
block (see EmptyResponseError, currentProvider, logger in
src/app/v1/_lib/proxy/forwarder.ts) is currently swallowed by the surrounding
catch; change the catch to detect and rethrow EmptyResponseError (e.g. if
(_parseError instanceof EmptyResponseError) throw _parseError) and only treat
other errors as JSON parse failures (log the debug about non-JSON response and
skip content check). This preserves retries/failover for missing_content while
still handling genuine parse errors.
🧹 Nitpick comments (1)
src/app/v1/_lib/proxy/forwarder.ts (1)

719-719: 冗余的 !isSSE 条件判断。

Line 694 的 if (isSSE) 分支已经 return response,执行到 line 719 时 isSSE 必定为 false,此处 if (!isSSE) 恒为真,可简化移除外层 if 减少嵌套层级。

@tesgth032 tesgth032 force-pushed the fix/fake-200-html-issue-749 branch from dcfac4a to c9bafaf Compare February 11, 2026 06:00
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +1 to +3
import { beforeEach, describe, expect, test, vi } from "vitest";

const mocks = vi.hoisted(() => {
Copy link

Choose a reason for hiding this comment

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

Inconsistent line endings

This new test file appears to be committed with CRLF line endings (visible as \r in the diff). In this repo the other TS files shown are LF-only, so this will create noisy diffs and can break tooling that enforces end-of-line normalization. Please re-save/normalize this file to LF and recommit.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/proxy/proxy-forwarder-fake-200-html.test.ts
Line: 1:3

Comment:
**Inconsistent line endings**

This new test file appears to be committed with CRLF line endings (visible as `\r` in the diff). In this repo the other TS files shown are LF-only, so this will create noisy diffs and can break tooling that enforces end-of-line normalization. Please re-save/normalize this file to LF and recommit.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link

greptile-apps bot commented Feb 11, 2026

Additional Comments (1)

tests/unit/lib/provider-endpoints/probe.test.ts
Missing endpoint-circuit mocks

Several tests now add getEndpointCircuitStateSync/resetEndpointCircuit to the @/lib/endpoint-circuit-breaker mock, but at least one test case (e.g. around here in the 5xx/4xx section) still mocks only recordEndpointFailure. Since src/lib/provider-endpoints/probe.ts imports getEndpointCircuitStateSync and resetEndpointCircuit at module scope, those tests will throw at runtime when probe.ts is imported. Make the endpoint-circuit-breaker mock include all required exports in every test that imports probeEndpointUrl/probeProviderEndpointAndRecord.

Also appears in: tests/unit/lib/provider-endpoints/probe.test.ts:112-114

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/lib/provider-endpoints/probe.test.ts
Line: 71:73

Comment:
**Missing endpoint-circuit mocks**

Several tests now add `getEndpointCircuitStateSync`/`resetEndpointCircuit` to the `@/lib/endpoint-circuit-breaker` mock, but at least one test case (e.g. around here in the 5xx/4xx section) still mocks only `recordEndpointFailure`. Since `src/lib/provider-endpoints/probe.ts` imports `getEndpointCircuitStateSync` and `resetEndpointCircuit` at module scope, those tests will throw at runtime when `probe.ts` is imported. Make the endpoint-circuit-breaker mock include all required exports in every test that imports `probeEndpointUrl`/`probeProviderEndpointAndRecord`.

Also appears in: tests/unit/lib/provider-endpoints/probe.test.ts:112-114

How can I resolve this? If you propose a fix, please make it concise.

@tesgth032
Copy link
Contributor Author

该 PR 与 #765 内容重复。为避免维护两套改动,我已基于最新 dev 重新整理并补齐 CI/测试修复,建议以 #765 为准;此 PR 先关闭。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core bug Something isn't working size/M Medium PR (< 500 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant