From 9bc598737164d07577c391536a01d16c429477f5 Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Mon, 4 May 2026 03:59:54 +0800 Subject: [PATCH] fix(shared): diagnose blocked 403 generation errors --- .changeset/gateway-waf-blocked-diagnostic.md | 7 ++++++ apps/desktop/src/renderer/src/store.test.ts | 24 ++++++++++++++++++ packages/i18n/src/locales/en.json | 2 ++ packages/i18n/src/locales/es.json | 2 ++ packages/i18n/src/locales/pt-BR.json | 2 ++ packages/i18n/src/locales/zh-CN.json | 2 ++ packages/shared/src/diagnostics.test.ts | 13 +++++++++- packages/shared/src/diagnostics.ts | 26 ++++++++++++++++++++ 8 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 .changeset/gateway-waf-blocked-diagnostic.md diff --git a/.changeset/gateway-waf-blocked-diagnostic.md b/.changeset/gateway-waf-blocked-diagnostic.md new file mode 100644 index 000000000..03e3c5fbe --- /dev/null +++ b/.changeset/gateway-waf-blocked-diagnostic.md @@ -0,0 +1,7 @@ +--- +'@open-codesign/shared': patch +'@open-codesign/desktop': patch +'@open-codesign/i18n': patch +--- + +Classify 403 generation responses that say requests were blocked as gateway or reverse-proxy blocks instead of invalid API keys. diff --git a/apps/desktop/src/renderer/src/store.test.ts b/apps/desktop/src/renderer/src/store.test.ts index 12520cca6..d79286102 100644 --- a/apps/desktop/src/renderer/src/store.test.ts +++ b/apps/desktop/src/renderer/src/store.test.ts @@ -1390,6 +1390,30 @@ describe('applyGenerateError via sendPrompt', () => { }); }); + it('classifies 403 blocked generation errors as gateway WAF blocks', async () => { + const err = Object.assign( + new Error( + "Error invoking remote method 'codesign:v1:generate': CodesignError: 403 Your request was blocked.", + ), + { + code: 'PROVIDER_ERROR', + upstream_provider: 'anthropic', + upstream_status: 403, + upstream_baseurl: 'https://relay.example.com/v1', + }, + ); + + await runFailingGenerate(err); + + const state = useCodesignStore.getState(); + expect(state.toasts[0]?.description).toContain('gateway or reverse proxy blocked'); + expect(state.toasts[0]?.description).not.toContain('API key invalid'); + expect(state.reportableErrors[0]?.context).toMatchObject({ + diagnostic_category: 'gateway-waf-blocked', + display_message: '403 Your request was blocked.', + }); + }); + it('cleans Electron IPC wrapper for toast/chat while preserving raw report context', async () => { const rawMessage = "Error invoking remote method 'codesign:v1:generate': CodesignError: 404 model 'models/gemini-2.5-pro' not found"; diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 38bf9b5b9..063562903 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -863,6 +863,7 @@ "sslError": "SSL / certificate error (self-signed cert on relay?).", "endpointNotFound": "The endpoint path exists in the Base URL but the provider did not expose this route.", "gatewayIncompatible": "The gateway accepted the connection but does not implement this provider's API. Try switching wire (e.g. openai-chat).", + "gatewayWafBlocked": "The gateway or reverse proxy blocked the generation request before it reached the model. Test Connection can still pass because it only probes the /models endpoint.", "openaiResponsesMisconfigured": "The endpoint rejected the request shape. The wire may be wrong — try switching to openai-chat.", "unsupportedRole": "The endpoint rejected the role format sent to the model. This is usually a wire or model-policy mismatch.", "reasoningPolicy": "The endpoint rejected reasoning metadata for this model. Lower the reasoning depth and try again.", @@ -891,6 +892,7 @@ "disableReasoning": "Turn reasoning off", "normalizeModelId": "Remove model prefix", "checkReferenceUrl": "Check reference URL", + "gatewayWafBlocked": "Check proxy allowlist / headers", "relayStreamingBug": "Upgrade the relay, switch wire to openai-chat, or use api.openai.com directly" }, "applyFix": "Apply this fix", diff --git a/packages/i18n/src/locales/es.json b/packages/i18n/src/locales/es.json index c854294c2..ced609c3d 100644 --- a/packages/i18n/src/locales/es.json +++ b/packages/i18n/src/locales/es.json @@ -858,6 +858,7 @@ "corsError": "Error de CORS (no debería pasar en el proceso principal). Esto es un error.", "sslError": "Error de SSL / certificado (¿certificado autofirmado en la retransmisión?).", "gatewayIncompatible": "La pasarela aceptó la conexión pero no implementa la API de este proveedor. Intenta cambiar de protocolo (ej. openai-chat).", + "gatewayWafBlocked": "La pasarela o el proxy inverso bloqueó la generación antes de llegar al modelo. Test Connection puede pasar porque solo prueba el endpoint /models.", "openaiResponsesMisconfigured": "El punto final rechazó la forma de la solicitud. El protocolo puede ser incorrecto — intenta cambiar a openai-chat.", "relayStreamingBug": "La pasarela puede manejar mal los eventos SSE de la API OpenAI Responses (versiones antiguas de sub2api / claude2api / anyrouter cortan el flujo prematuramente).", "serverError": "Error del servidor ascendente. Puede ser transitorio — intenta de nuevo.", @@ -874,6 +875,7 @@ "reportBug": "Reportar este error", "disableTls": "Desactivar verificación TLS", "switchWire": "Cambiar protocolo en Configuración", + "gatewayWafBlocked": "Revisar allowlist / headers del proxy", "relayStreamingBug": "Actualiza la retransmisión, cambia el protocolo a openai-chat o usa api.openai.com directamente" }, "applyFix": "Aplicar esta corrección", diff --git a/packages/i18n/src/locales/pt-BR.json b/packages/i18n/src/locales/pt-BR.json index 2ce4df19f..b44006ae6 100644 --- a/packages/i18n/src/locales/pt-BR.json +++ b/packages/i18n/src/locales/pt-BR.json @@ -823,6 +823,7 @@ "sslError": "Erro de SSL / certificado (certificado autoassinado no relay?).", "endpointNotFound": "A URL base já contém um caminho de versão, mas o provedor não expôs esta rota.", "gatewayIncompatible": "O gateway aceitou a conexão, mas não implementa a API deste provedor. Tente trocar o wire, por exemplo para openai-chat.", + "gatewayWafBlocked": "O gateway ou proxy reverso bloqueou a geração antes de ela chegar ao modelo. Test Connection ainda pode passar porque só verifica o endpoint /models.", "openaiResponsesMisconfigured": "O endpoint rejeitou o formato da requisição. O wire pode estar errado; tente openai-chat.", "unsupportedRole": "O endpoint rejeitou o formato de role enviado ao modelo. Normalmente é incompatibilidade de wire ou política do modelo.", "reasoningPolicy": "O endpoint rejeitou metadados de reasoning para este modelo. Reduza a profundidade de reasoning e tente novamente.", @@ -851,6 +852,7 @@ "disableReasoning": "Desativar reasoning", "normalizeModelId": "Remover prefixo do modelo", "checkReferenceUrl": "Verificar URL de referência", + "gatewayWafBlocked": "Verificar allowlist / headers do proxy", "relayStreamingBug": "Atualize o relay, troque o wire para openai-chat ou use api.openai.com diretamente" }, "applyFix": "Aplicar esta correção", diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json index 48e6ef9fc..0509cfb9a 100644 --- a/packages/i18n/src/locales/zh-CN.json +++ b/packages/i18n/src/locales/zh-CN.json @@ -859,6 +859,7 @@ "sslError": "SSL / 证书错误(中转服务使用了自签证书?)。", "endpointNotFound": "Base URL 已包含版本路径,但 Provider 没有暴露这个接口路径。", "gatewayIncompatible": "网关接受了连接但没有实现该 Provider 的 API。尝试切换 wire(例如改为 openai-chat)。", + "gatewayWafBlocked": "网关或反代在请求到达模型前拦截了生成请求。测试连接可能仍然通过,因为它只探测 /models 端点。", "openaiResponsesMisconfigured": "端点拒绝了请求格式。wire 可能配错了——尝试切换到 openai-chat。", "unsupportedRole": "端点拒绝了发送给模型的 role 格式,通常是 wire 或模型策略不兼容。", "reasoningPolicy": "端点拒绝了该模型的 reasoning 元数据。请降低 reasoning 深度后重试。", @@ -887,6 +888,7 @@ "disableReasoning": "关闭 reasoning", "normalizeModelId": "移除模型前缀", "checkReferenceUrl": "检查 Reference URL", + "gatewayWafBlocked": "检查代理白名单 / 请求头", "relayStreamingBug": "升级中转服务;或把 wire 切到 openai-chat;或改用 api.openai.com" }, "applyFix": "应用此修复", diff --git a/packages/shared/src/diagnostics.test.ts b/packages/shared/src/diagnostics.test.ts index 84fcbb15d..be886edc1 100644 --- a/packages/shared/src/diagnostics.test.ts +++ b/packages/shared/src/diagnostics.test.ts @@ -173,12 +173,23 @@ describe('diagnoseGenerateFailure', () => { expect(result[0]?.cause).toBe('diagnostics.cause.keyInvalid'); }); - it('maps 403 to keyInvalid hypothesis', () => { + it('maps plain 403 to keyInvalid hypothesis', () => { const result = diagnoseGenerateFailure({ ...ctx, status: 403 }); expect(result[0]?.cause).toBe('diagnostics.cause.keyInvalid'); expect(result[0]?.category).toBe('auth'); }); + it('maps 403 blocked generation responses to gatewayWafBlocked', () => { + const result = diagnoseGenerateFailure({ + ...ctx, + status: 403, + message: '403 Your request was blocked.', + }); + expect(result[0]?.cause).toBe('diagnostics.cause.gatewayWafBlocked'); + expect(result[0]?.category).toBe('gateway-waf-blocked'); + expect(result[0]?.suggestedFix?.label).toBe('diagnostics.fix.gatewayWafBlocked'); + }); + it('maps 422 developer role rejection to unsupported-role with a wire switch fix', () => { const result = diagnoseGenerateFailure({ ...ctx, diff --git a/packages/shared/src/diagnostics.ts b/packages/shared/src/diagnostics.ts index d4ef4c12d..13848d340 100644 --- a/packages/shared/src/diagnostics.ts +++ b/packages/shared/src/diagnostics.ts @@ -30,6 +30,7 @@ export type DiagnosticCategory = | 'reasoning-policy' | 'model-id-shape' | 'relay-stream-cutoff' + | 'gateway-waf-blocked' | 'model-discovery-degraded' | 'transport-interrupted' | 'upstream-server-error' @@ -312,6 +313,16 @@ function isCustomBaseUrl(baseUrl: string | undefined): boolean { } } +function looksLikeGatewayWafBlock(message: string): boolean { + return ( + /\byour request was blocked\b/i.test(message) || + /\brequest blocked\b/i.test(message) || + /\bwaf\b/i.test(message) || + /\bcloudflare\b/i.test(message) || + /\bray id\b/i.test(message) + ); +} + export function diagnoseGenerateFailure(ctx: GenerateFailureContext): DiagnosticHypothesis[] { const message = (ctx.message ?? '').toLowerCase(); const status = ctx.status; @@ -461,6 +472,21 @@ export function diagnoseGenerateFailure(ctx: GenerateFailureContext): Diagnostic ]; } + if (status === 403 && looksLikeGatewayWafBlock(message)) { + return [ + h({ + cause: 'diagnostics.cause.gatewayWafBlocked', + category: 'gateway-waf-blocked', + severity: 'warning', + suggestedFix: { + kind: 'openSettings', + label: 'diagnostics.fix.gatewayWafBlocked', + settingsTab: 'models', + }, + }), + ]; + } + if (status === 400 && message.includes('instructions')) { return [ h({