From 3e19a08aa815168eab3565b9cacef655a3fba750 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Tue, 23 Dec 2025 17:09:04 +0000 Subject: [PATCH] refactor: improve file upload error handling and return errors array Replace errorMessage string with uploadErrors array in processAttachments to provide structured error handling. Enhanced error messages to include file names. Added getHeader helper method for case-insensitive header lookups. Fixed Gemini upload to use contentType parameter instead of Content-Type header. --- AIClasses/BaseAIClass.ts | 18 ++++++------------ AIClasses/BaseAIFileService.ts | 5 +++++ AIClasses/Claude/Claude.ts | 6 +++--- AIClasses/Gemini/Gemini.ts | 7 ++++--- AIClasses/Gemini/GeminiFileService.ts | 4 ++-- AIClasses/OpenAI/OpenAI.ts | 6 +++--- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/AIClasses/BaseAIClass.ts b/AIClasses/BaseAIClass.ts index 8b4d050..3028356 100644 --- a/AIClasses/BaseAIClass.ts +++ b/AIClasses/BaseAIClass.ts @@ -133,33 +133,27 @@ export abstract class BaseAIClass implements IAIClass { protected async processAttachments( attachments: Attachment[], formatBinaryFiles: (attachments: Attachment[]) => string - ): Promise<{ formattedParts: T[], errorMessage?: string }> { - const failedUploads: string[] = []; + ): Promise<{ formattedParts: T[], uploadErrors: Error[] }> { + const uploadErrors: Error[] = []; for (const attachment of attachments) { try { if (attachment.base64.trim() === "") { - Exception.throw("File has no content!"); + Exception.throw(`Failed to upload ${attachment.fileName}: File has no content`); } await this.aiFileService.uploadFile(attachment); if (!attachment.getFileID(this.provider)) { - Exception.throw("File ID undefined after upload attempt"); + Exception.throw(`Failed to upload ${attachment.fileName}: File ID undefined after upload attempt`); } } catch (error) { - Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`); - failedUploads.push(attachment.fileName); + uploadErrors.push(Exception.new(error)); } } const formattedContent = formatBinaryFiles(attachments); const formattedParts = JSON.parse(formattedContent) as T[]; - return { - formattedParts, - errorMessage: failedUploads.length > 0 - ? `[Upload failed for: ${failedUploads.join(', ')}.]` - : undefined - }; + return { formattedParts, uploadErrors }; } /** diff --git a/AIClasses/BaseAIFileService.ts b/AIClasses/BaseAIFileService.ts index 817d490..7b94872 100644 --- a/AIClasses/BaseAIFileService.ts +++ b/AIClasses/BaseAIFileService.ts @@ -160,4 +160,9 @@ export abstract class BaseAIFileService implements IAIFileService { return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); } + protected getHeader(headerName: string, headers: Record): string | undefined { + const header = Object.keys(headers).find(header => header.toLowerCase() === headerName.toLowerCase()); + return header ? headers[header] : undefined; + } + } diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index 1758a9e..0d687c3 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -205,17 +205,17 @@ export class Claude extends BaseAIClass { // Add binary file attachments if present if (content.attachments && content.attachments.length > 0) { - const { formattedParts, errorMessage } = await this.processAttachments( + const { formattedParts, uploadErrors } = await this.processAttachments( content.attachments, (attachments) => this.formatBinaryFiles(attachments) ); contentBlocks.push(...formattedParts); - if (errorMessage) { + for (const uploadError of uploadErrors) { contentBlocks.push({ type: "text", - text: errorMessage + text: Exception.messageFrom(uploadError) }); } } diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index b8a841c..4a12dcd 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -13,6 +13,7 @@ import { FinishReason } from "@google/genai"; import { MimeType, toMimeType } from "Enums/MimeType"; import { isTextFile } from "Enums/FileType"; import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping"; +import { Exception } from "Helpers/Exception"; export class Gemini extends BaseAIClass { @@ -250,16 +251,16 @@ export class Gemini extends BaseAIClass { // Add binary file attachments if present if (content.attachments && content.attachments.length > 0) { - const { formattedParts, errorMessage } = await this.processAttachments( + const { formattedParts, uploadErrors } = await this.processAttachments( content.attachments, (attachments) => this.formatBinaryFiles(attachments) ); parts.push(...formattedParts); - if (errorMessage) { + for (const uploadError of uploadErrors) { parts.push({ - text: errorMessage + text: Exception.messageFrom(uploadError) }); } } diff --git a/AIClasses/Gemini/GeminiFileService.ts b/AIClasses/Gemini/GeminiFileService.ts index eb91944..c9425c5 100644 --- a/AIClasses/Gemini/GeminiFileService.ts +++ b/AIClasses/Gemini/GeminiFileService.ts @@ -64,7 +64,7 @@ export class GeminiFileService extends BaseAIFileService { throw ApiError.fromResponse(initiateResponse.status, "Upload file failed", initiateResponse.text); } - const uploadUrl = initiateResponse.headers["x-goog-upload-url"]; + const uploadUrl = this.getHeader("x-goog-upload-url", initiateResponse.headers); if (!uploadUrl) { Exception.throw("No upload URL received from initiate request"); } @@ -73,8 +73,8 @@ export class GeminiFileService extends BaseAIFileService { const uploadResponse = await requestUrl({ url: uploadUrl, method: "POST", + contentType: mimeType, headers: { - "Content-Type": mimeType, "X-Goog-Upload-Offset": "0", "X-Goog-Upload-Command": "upload, finalize" }, diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index dfbe0bf..904c580 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -246,18 +246,18 @@ export class OpenAI extends BaseAIClass { // Case 2: Binary file attachments if (content.attachments && content.attachments.length > 0) { - const { formattedParts, errorMessage } = await this.processAttachments( + const { formattedParts, uploadErrors } = await this.processAttachments( content.attachments, (attachments) => this.formatBinaryFiles(attachments) ); results.push(...formattedParts); - if (errorMessage) { + for (const uploadError of uploadErrors) { // OpenAI formatBinaryFiles returns array with role wrapper, so add as separate message results.push({ role: "user", - content: errorMessage + content: Exception.messageFrom(uploadError) }); } continue;