From c95338232a37981b95b785b47185ce18d6d94b7a Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Mon, 1 Dec 2025 03:18:53 -0500 Subject: [PATCH] fix(glmt): handle 401 errors and headers-already-sent exception (#30) - add res.headersSent check before writing error headers - write error as SSE event when headers already sent (streaming mode) - add parseUpstreamError() for user-friendly error messages - return proper HTTP status codes (401, 403, 429, 502, 504) - clear auth error: check ANTHROPIC_AUTH_TOKEN in config Fixes #26 --- src/glmt/glmt-proxy.ts | 93 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/src/glmt/glmt-proxy.ts b/src/glmt/glmt-proxy.ts index b9083b57..2062e3d1 100644 --- a/src/glmt/glmt-proxy.ts +++ b/src/glmt/glmt-proxy.ts @@ -208,12 +208,25 @@ export class GlmtProxy { const duration = Date.now() - startTime; this.log(`Request failed after ${duration}ms: ${err.message}`); - res.writeHead(500, { 'Content-Type': 'application/json' }); + // Parse error to provide clearer messages + const errorInfo = this.parseUpstreamError(err); + + // Check if headers already sent (streaming mode may have started) + if (res.headersSent) { + // Headers already sent, write error as SSE event and close + this.log('Headers already sent, writing error as SSE event'); + res.write(`event: error\n`); + res.write(`data: ${JSON.stringify({ error: errorInfo })}\n\n`); + res.end(); + return; + } + + res.writeHead(errorInfo.statusCode, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ error: { - type: 'proxy_error', - message: err.message, + type: errorInfo.type, + message: errorInfo.message, }, }) ); @@ -526,6 +539,80 @@ export class GlmtProxy { console.error(`[glmt-proxy] ${message}`); } } + + /** + * Parse upstream error and return user-friendly error info + */ + private parseUpstreamError(err: Error): { + statusCode: number; + type: string; + message: string; + } { + const errorMessage = err.message; + + // Check for 401 Unauthorized / auth token missing + if ( + errorMessage.includes('401') || + errorMessage.toLowerCase().includes('unauthorized') || + errorMessage.toLowerCase().includes('authorization token missing') || + errorMessage.toLowerCase().includes('invalid token') + ) { + return { + statusCode: 401, + type: 'authentication_error', + message: + 'Authorization token missing or invalid. Please check your ANTHROPIC_AUTH_TOKEN environment variable in ~/.ccs/config.json or settings.json.', + }; + } + + // Check for 403 Forbidden + if (errorMessage.includes('403') || errorMessage.toLowerCase().includes('forbidden')) { + return { + statusCode: 403, + type: 'permission_error', + message: 'Access forbidden. Your token may not have permission to access this resource.', + }; + } + + // Check for 429 Rate limit + if (errorMessage.includes('429') || errorMessage.toLowerCase().includes('rate limit')) { + return { + statusCode: 429, + type: 'rate_limit_error', + message: 'Rate limit exceeded. Please wait before making more requests.', + }; + } + + // Check for timeout + if (errorMessage.toLowerCase().includes('timeout')) { + return { + statusCode: 504, + type: 'timeout_error', + message: 'Request timed out. The upstream server took too long to respond.', + }; + } + + // Check for connection errors + if ( + errorMessage.toLowerCase().includes('econnrefused') || + errorMessage.toLowerCase().includes('enotfound') || + errorMessage.toLowerCase().includes('connection') + ) { + return { + statusCode: 502, + type: 'connection_error', + message: + 'Failed to connect to upstream server. Please check your network and ANTHROPIC_BASE_URL configuration.', + }; + } + + // Default: proxy error + return { + statusCode: 500, + type: 'proxy_error', + message: errorMessage, + }; + } } // Main entry point