Browse Source

fix(proxy): address bugbot review comments on fake-200 error handling

- Add i18n for HTTP status prefix in LogicTraceTab (5 languages)
- Wrap verbose details gathering in try-catch to prevent cascading failures
- Truncate rawBody to 4096 chars before sanitization in error-handler
- Tighten not_found regex to require contextual prefixes, preventing false 404 inference
- Add debug logging to silent catch blocks in readResponseTextUpTo
- Add test assertion for fake200DetectedReason display
ding113 3 weeks ago
parent
commit
f0b99a40c1

+ 1 - 0
messages/en/dashboard.json

@@ -327,6 +327,7 @@
         "prioritySelection": "Priority Selection",
         "attemptProvider": "Attempt: {provider}",
         "retryAttempt": "Retry #{number}",
+        "httpStatus": "HTTP {code}{inferredSuffix}",
         "sessionReuse": "Session Reuse",
         "sessionReuseDesc": "Reusing provider from session cache",
         "sessionReuseTitle": "Session Binding",

+ 1 - 0
messages/ja/dashboard.json

@@ -327,6 +327,7 @@
         "prioritySelection": "優先度選択",
         "attemptProvider": "試行: {provider}",
         "retryAttempt": "再試行 #{number}",
+        "httpStatus": "HTTP {code}{inferredSuffix}",
         "sessionReuse": "セッション再利用",
         "sessionReuseDesc": "セッションキャッシュからプロバイダーを再利用",
         "sessionReuseTitle": "セッションバインディング",

+ 1 - 0
messages/ru/dashboard.json

@@ -327,6 +327,7 @@
         "prioritySelection": "Выбор по приоритету",
         "attemptProvider": "Попытка: {provider}",
         "retryAttempt": "Повтор #{number}",
+        "httpStatus": "HTTP {code}{inferredSuffix}",
         "sessionReuse": "Повторное использование сессии",
         "sessionReuseDesc": "Провайдер из кэша сессии",
         "sessionReuseTitle": "Привязка сессии",

+ 1 - 0
messages/zh-CN/dashboard.json

@@ -327,6 +327,7 @@
         "prioritySelection": "优先级选择",
         "attemptProvider": "尝试: {provider}",
         "retryAttempt": "重试 #{number}",
+        "httpStatus": "HTTP {code}{inferredSuffix}",
         "sessionReuse": "会话复用",
         "sessionReuseDesc": "从会话缓存复用供应商",
         "sessionReuseTitle": "会话绑定",

+ 1 - 0
messages/zh-TW/dashboard.json

@@ -327,6 +327,7 @@
         "prioritySelection": "優先順序選擇",
         "attemptProvider": "嘗試: {provider}",
         "retryAttempt": "重試 #{number}",
+        "httpStatus": "HTTP {code}{inferredSuffix}",
         "sessionReuse": "會話複用",
         "sessionReuseDesc": "從會話快取複用供應商",
         "sessionReuseTitle": "會話綁定",

+ 2 - 0
src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx

@@ -245,6 +245,7 @@ const messages = {
           prioritySelection: "Priority Selection",
           attemptProvider: "Attempt: {provider}",
           retryAttempt: "Retry #{number}",
+          httpStatus: "HTTP {code}{inferredSuffix}",
         },
         noError: {
           processing: "No error (processing)",
@@ -348,6 +349,7 @@ describe("error-details-dialog layout", () => {
 
     expect(html).toContain("FAKE_200_EMPTY_BODY");
     expect(html).toContain("Note: detected after stream end; payload may have been forwarded");
+    expect(html).toContain("Detected reason: Empty response body");
   });
 
   test("renders special settings section when specialSettings exists", () => {

+ 12 - 2
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx

@@ -466,10 +466,20 @@ export function LogicTraceTab({
                 subtitle={
                   isSessionReuse
                     ? item.statusCode
-                      ? `HTTP ${item.statusCode}${item.statusCodeInferred ? ` ${t("statusCodeInferredSuffix")}` : ""}`
+                      ? t("logicTrace.httpStatus", {
+                          code: item.statusCode,
+                          inferredSuffix: item.statusCodeInferred
+                            ? ` ${t("statusCodeInferredSuffix")}`
+                            : "",
+                        })
                       : item.name
                     : item.statusCode
-                      ? `HTTP ${item.statusCode}${item.statusCodeInferred ? ` ${t("statusCodeInferredSuffix")}` : ""}`
+                      ? t("logicTrace.httpStatus", {
+                          code: item.statusCode,
+                          inferredSuffix: item.statusCodeInferred
+                            ? ` ${t("statusCodeInferredSuffix")}`
+                            : "",
+                        })
                       : item.reason
                         ? tChain(`reasons.${item.reason}`)
                         : undefined

+ 40 - 31
src/app/v1/_lib/proxy/error-handler.ts

@@ -250,38 +250,47 @@ export class ProxyErrorHandler {
       isEmptyResponseError(error);
 
     if (shouldAttachVerboseDetails) {
-      const settings = await getCachedSystemSettings();
-      if (settings.verboseProviderError) {
-        if (error instanceof ProxyError) {
-          upstreamRequestId = error.upstreamError?.requestId;
-          const rawBody =
-            typeof error.upstreamError?.rawBody === "string" && error.upstreamError.rawBody
-              ? sanitizeErrorTextForDetail(error.upstreamError.rawBody)
-              : error.upstreamError?.rawBody;
-          details = {
-            upstreamError: {
-              kind: "fake_200",
-              code: error.message,
-              statusCode: error.statusCode,
-              statusCodeInferred: error.upstreamError?.statusCodeInferred ?? false,
-              statusCodeInferenceMatcherId:
-                error.upstreamError?.statusCodeInferenceMatcherId ?? null,
-              clientSafeMessage: error.getClientSafeMessage(),
-              rawBody,
-              rawBodyTruncated: error.upstreamError?.rawBodyTruncated ?? false,
-            },
-          };
-        } else if (isEmptyResponseError(error)) {
-          details = {
-            upstreamError: {
-              kind: "empty_response",
-              reason: error.reason,
-              clientSafeMessage: error.getClientSafeMessage(),
-              rawBody: "",
-              rawBodyTruncated: false,
-            },
-          };
+      try {
+        const settings = await getCachedSystemSettings();
+        if (settings.verboseProviderError) {
+          if (error instanceof ProxyError) {
+            upstreamRequestId = error.upstreamError?.requestId;
+            const rawBodySrc = error.upstreamError?.rawBody;
+            const rawBody =
+              typeof rawBodySrc === "string" && rawBodySrc
+                ? sanitizeErrorTextForDetail(
+                    rawBodySrc.length > 4096 ? rawBodySrc.slice(0, 4096) : rawBodySrc
+                  )
+                : rawBodySrc;
+            details = {
+              upstreamError: {
+                kind: "fake_200",
+                code: error.message,
+                statusCode: error.statusCode,
+                statusCodeInferred: error.upstreamError?.statusCodeInferred ?? false,
+                statusCodeInferenceMatcherId:
+                  error.upstreamError?.statusCodeInferenceMatcherId ?? null,
+                clientSafeMessage: error.getClientSafeMessage(),
+                rawBody,
+                rawBodyTruncated: error.upstreamError?.rawBodyTruncated ?? false,
+              },
+            };
+          } else if (isEmptyResponseError(error)) {
+            details = {
+              upstreamError: {
+                kind: "empty_response",
+                reason: error.reason,
+                clientSafeMessage: error.getClientSafeMessage(),
+                rawBody: "",
+                rawBodyTruncated: false,
+              },
+            };
+          }
         }
+      } catch (verboseError) {
+        logger.warn("ProxyErrorHandler: failed to gather verbose details, skipping", {
+          error: verboseError instanceof Error ? verboseError.message : String(verboseError),
+        });
       }
     }
 

+ 4 - 4
src/app/v1/_lib/proxy/forwarder.ts

@@ -148,15 +148,15 @@ async function readResponseTextUpTo(
     if (truncated) {
       try {
         await reader.cancel();
-      } catch {
-        // ignore
+      } catch (cancelErr) {
+        logger.debug("readResponseTextUpTo: failed to cancel reader", { error: cancelErr });
       }
     }
 
     try {
       reader.releaseLock();
-    } catch {
-      // ignore
+    } catch (releaseErr) {
+      logger.debug("readResponseTextUpTo: failed to release reader lock", { error: releaseErr });
     }
   }
 

+ 1 - 1
src/lib/utils/upstream-error-detection.ts

@@ -115,7 +115,7 @@ const ERROR_STATUS_MATCHERS: Array<{ statusCode: number; matcherId: string; re:
   {
     statusCode: 404,
     matcherId: "not_found",
-    re: /(?:\bHTTP\/\d(?:\.\d)?\s+404\b|\bnot\s+found\b|\b(?:model|deployment|endpoint|resource)\s+not\s+found\b|\bunknown\s+model\b|\bdoes\s+not\s+exist\b|\bNOT_FOUND\b|\bResourceNotFoundException\b|未找到|不存在|模型不存在)/iu,
+    re: /(?:\bHTTP\/\d(?:\.\d)?\s+404\b|\b(?:model|deployment|endpoint|resource|route|path|api|service|url)\s+not\s+found\b|\bunknown\s+model\b|\bdoes\s+not\s+exist\b|\bNOT_FOUND\b|\bResourceNotFoundException\b|未找到|不存在|模型不存在)/iu,
   },
   {
     statusCode: 413,