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.
This commit is contained in:
Andrew Beal 2025-12-23 17:09:04 +00:00
parent 7c1d222b44
commit 3e19a08aa8
6 changed files with 23 additions and 23 deletions

View file

@ -133,33 +133,27 @@ export abstract class BaseAIClass implements IAIClass {
protected async processAttachments<T>(
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 };
}
/**

View file

@ -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, string>): string | undefined {
const header = Object.keys(headers).find(header => header.toLowerCase() === headerName.toLowerCase());
return header ? headers[header] : undefined;
}
}

View file

@ -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<ContentBlockParam>(
const { formattedParts, uploadErrors } = await this.processAttachments<ContentBlockParam>(
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)
});
}
}

View file

@ -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<Part>(
const { formattedParts, uploadErrors } = await this.processAttachments<Part>(
content.attachments,
(attachments) => this.formatBinaryFiles(attachments)
);
parts.push(...formattedParts);
if (errorMessage) {
for (const uploadError of uploadErrors) {
parts.push({
text: errorMessage
text: Exception.messageFrom(uploadError)
});
}
}

View file

@ -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"
},

View file

@ -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<ResponsesAPIInput>(
const { formattedParts, uploadErrors } = await this.processAttachments<ResponsesAPIInput>(
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;