Skip to content

Commit 65e487f

Browse files
committed
Add error handling for missing/invalid Voyage API key
- Add custom error classes (VoyageAuthError, VoyageAPIError) to distinguish error types - Update generateVoyageEmbedding to throw VoyageAuthError for 401 responses - Return 401 status for authentication errors, 503 for API errors - Improve client-side error handling with user-friendly messages - Update tests to verify proper error handling for different scenarios
1 parent d3148f9 commit 65e487f

6 files changed

Lines changed: 131 additions & 22 deletions

File tree

mflix/client/app/aggregations/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
2-
import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '../lib/api';
3-
import { MovieWithComments, YearlyStats, DirectorStats } from '../types/aggregations';
2+
import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '@/lib/api';
3+
import { MovieWithComments, YearlyStats, DirectorStats } from '@/types/aggregations';
44
import styles from './aggregations.module.css';
55

66
export default async function AggregationsPage() {

mflix/client/app/lib/api.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -680,16 +680,36 @@ export async function vectorSearchMovies(searchParams: {
680680
const result = await response.json();
681681

682682
if (!response.ok) {
683-
return {
684-
success: false,
685-
error: result.error || `Failed to perform vector search: ${response.status}`
683+
// Extract error message from the standardized error response
684+
const errorMessage = result.message || result.error?.message || `Failed to perform vector search: ${response.status}`;
685+
const errorCode = result.error?.code;
686+
687+
// Provide user-friendly messages for specific error codes
688+
if (errorCode === 'VOYAGE_AUTH_ERROR') {
689+
return {
690+
success: false,
691+
error: 'Vector search unavailable: Your Voyage AI API key is missing or invalid. Please add a valid VOYAGE_API_KEY to your .env file and restart the server.'
692+
};
693+
}
694+
695+
if (errorCode === 'SERVICE_UNAVAILABLE' || errorCode === 'VOYAGE_API_ERROR') {
696+
return {
697+
success: false,
698+
error: errorMessage || 'Vector search service is currently unavailable. Please try again later.'
699+
};
700+
}
701+
702+
return {
703+
success: false,
704+
error: errorMessage
686705
};
687706
}
688707

689708
if (!result.success) {
690-
return {
691-
success: false,
692-
error: result.error || 'API returned error response'
709+
const errorMessage = result.message || result.error?.message || 'API returned error response';
710+
return {
711+
success: false,
712+
error: errorMessage
693713
};
694714
}
695715

mflix/client/app/movie/[id]/error.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useEffect } from 'react';
44
import Link from 'next/link';
5-
import { ROUTES } from '../../lib/constants';
5+
import { ROUTES } from '@/lib/constants';
66
import styles from './error.module.css';
77

88
export default function MovieDetailsError({

mflix/client/app/movie/[id]/loading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pageStyles from './page.module.css';
2-
import { MovieDetailsSkeleton } from '../../components/LoadingSkeleton';
2+
import { MovieDetailsSkeleton } from '@/components';
33

44
export default function MovieDetailsLoading() {
55
return (

mflix/server/js-express/src/controllers/movieController.ts

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,36 @@ export async function vectorSearchMovies(req: Request, res: Response): Promise<v
835835
);
836836
} catch (error) {
837837
console.error("Vector search error:", error);
838+
839+
// Handle Voyage AI authentication errors
840+
if (error instanceof VoyageAuthError) {
841+
res
842+
.status(401)
843+
.json(
844+
createErrorResponse(
845+
error.message,
846+
"VOYAGE_AUTH_ERROR",
847+
"Please verify your VOYAGE_API_KEY is correct in the .env file"
848+
)
849+
);
850+
return;
851+
}
852+
853+
// Handle other Voyage AI API errors
854+
if (error instanceof VoyageAPIError) {
855+
res
856+
.status(503)
857+
.json(
858+
createErrorResponse(
859+
"Vector search service unavailable",
860+
"VOYAGE_API_ERROR",
861+
error.message
862+
)
863+
);
864+
return;
865+
}
866+
867+
// Handle generic errors
838868
res
839869
.status(500)
840870
.json(
@@ -1160,16 +1190,41 @@ export async function getDirectorsWithMostMovies(
11601190
);
11611191
}
11621192

1193+
/**
1194+
* Custom error class for Voyage AI API authentication errors
1195+
*/
1196+
class VoyageAuthError extends Error {
1197+
constructor(message: string) {
1198+
super(message);
1199+
this.name = "VoyageAuthError";
1200+
}
1201+
}
1202+
1203+
/**
1204+
* Custom error class for Voyage AI API errors
1205+
*/
1206+
class VoyageAPIError extends Error {
1207+
public statusCode: number;
1208+
1209+
constructor(message: string, statusCode: number) {
1210+
super(message);
1211+
this.name = "VoyageAPIError";
1212+
this.statusCode = statusCode;
1213+
}
1214+
}
1215+
11631216
/**
11641217
* Generates a vector embedding using the Voyage AI REST API.
1165-
*
1218+
*
11661219
* This function calls the Voyage AI API directly to generate embeddings with 2048 dimensions.
11671220
* The voyage-3-large model supports multiple dimensions (256, 512, 1024, 2048), and we explicitly
11681221
* request 2048 to match the vector search index configuration.
1169-
*
1222+
*
11701223
* @param text The text to generate an embedding for
11711224
* @param apiKey The Voyage AI API key
11721225
* @returns Promise<number[]> representing the embedding vector
1226+
* @throws VoyageAuthError if the API key is invalid (401)
1227+
* @throws VoyageAPIError for other API errors
11731228
*/
11741229
async function generateVoyageEmbedding(text: string, apiKey: string): Promise<number[]> {
11751230
// Build the request body with output_dimension set to 2048
@@ -1192,21 +1247,36 @@ async function generateVoyageEmbedding(text: string, apiKey: string): Promise<nu
11921247

11931248
if (!response.ok) {
11941249
const errorText = await response.text();
1195-
throw new Error(`Voyage AI API returned status ${response.status}: ${errorText}`);
1250+
1251+
// Handle authentication errors specifically
1252+
if (response.status === 401) {
1253+
throw new VoyageAuthError("Invalid Voyage AI API key. Please check your VOYAGE_API_KEY in the .env file");
1254+
}
1255+
1256+
throw new VoyageAPIError(
1257+
`Voyage AI API returned status ${response.status}: ${errorText}`,
1258+
response.status
1259+
);
11961260
}
11971261

11981262
const data = await response.json() as VoyageAIResponse;
1199-
1263+
12001264
// Extract the embedding from the response
12011265
if (!data.data || !data.data[0] || !data.data[0].embedding) {
1202-
throw new Error("Invalid response format from Voyage AI API");
1266+
throw new VoyageAPIError("Invalid response format from Voyage AI API", 500);
12031267
}
12041268

12051269
return data.data[0].embedding;
12061270
} catch (error) {
1271+
// Re-throw our custom errors
1272+
if (error instanceof VoyageAuthError || error instanceof VoyageAPIError) {
1273+
throw error;
1274+
}
1275+
1276+
// Wrap other errors
12071277
if (error instanceof Error) {
1208-
throw new Error(`Failed to generate embedding: ${error.message}`);
1278+
throw new VoyageAPIError(`Failed to generate embedding: ${error.message}`, 500);
12091279
}
1210-
throw new Error("Failed to generate embedding: Unknown error");
1280+
throw new VoyageAPIError("Failed to generate embedding: Unknown error", 500);
12111281
}
12121282
}

mflix/server/js-express/tests/controllers/movieController.test.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ describe("Movie Controller Tests", () => {
880880
);
881881
});
882882

883-
it("should handle Voyage AI API errors", async () => {
883+
it("should handle Voyage AI authentication errors with 401 status", async () => {
884884
mockFetch.mockResolvedValueOnce({
885885
ok: false,
886886
status: 401,
@@ -891,11 +891,30 @@ describe("Movie Controller Tests", () => {
891891

892892
await vectorSearchMovies(mockRequest as Request, mockResponse as Response);
893893

894-
expect(mockStatus).toHaveBeenCalledWith(500);
894+
expect(mockStatus).toHaveBeenCalledWith(401);
895895
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
896-
"Error performing vector search",
897-
"VECTOR_SEARCH_ERROR",
898-
expect.stringContaining("Voyage AI API returned status 401")
896+
"Invalid Voyage AI API key. Please check your VOYAGE_API_KEY in the .env file",
897+
"VOYAGE_AUTH_ERROR",
898+
"Please verify your VOYAGE_API_KEY is correct in the .env file"
899+
);
900+
});
901+
902+
it("should handle other Voyage AI API errors with 503 status", async () => {
903+
mockFetch.mockResolvedValueOnce({
904+
ok: false,
905+
status: 500,
906+
text: () => Promise.resolve("Internal Server Error"),
907+
} as any);
908+
909+
mockRequest.query = { q: "test" };
910+
911+
await vectorSearchMovies(mockRequest as Request, mockResponse as Response);
912+
913+
expect(mockStatus).toHaveBeenCalledWith(503);
914+
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
915+
"Vector search service unavailable",
916+
"VOYAGE_API_ERROR",
917+
expect.stringContaining("Voyage AI API returned status 500")
899918
);
900919
});
901920

0 commit comments

Comments
 (0)