Skip to content

Commit c55c852

Browse files
committed
Add Voyage AI error handling improvements to Python and Java backends
- Python FastAPI backend: - Created custom exception classes (VoyageAuthError, VoyageAPIError) - Created error response utility function - Added global exception handlers in main.py - Updated vector search endpoint to use custom exceptions - Returns 400 for missing API key (SERVICE_UNAVAILABLE) - Returns 401 for invalid API key (VOYAGE_AUTH_ERROR) - Returns 503 for Voyage AI API errors (VOYAGE_API_ERROR) - Updated test to match new error handling behavior - Java Spring backend: - Created custom exception classes (VoyageAuthException, VoyageAPIException, ServiceUnavailableException) - Updated GlobalExceptionHandler with handlers for new exceptions - Updated MovieServiceImpl to throw custom exceptions - Returns 400 for missing API key (SERVICE_UNAVAILABLE) - Returns 401 for invalid API key (VOYAGE_AUTH_ERROR) - Returns 503 for Voyage AI API errors (VOYAGE_API_ERROR) - Updated tests to expect ServiceUnavailableException instead of ValidationException All three backends (Express, Python, Java) now have consistent Voyage AI error handling.
1 parent 93b48f3 commit c55c852

12 files changed

Lines changed: 304 additions & 24 deletions

File tree

mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,61 @@ public ResponseEntity<ErrorResponse> handleMissingServletRequestParameter(
7979
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
8080
}
8181

82+
@ExceptionHandler(ServiceUnavailableException.class)
83+
public ResponseEntity<ErrorResponse> handleServiceUnavailableException(
84+
ServiceUnavailableException ex, WebRequest request) {
85+
logger.error("Service unavailable: {}", ex.getMessage());
86+
87+
ErrorResponse errorResponse = ErrorResponse.builder()
88+
.success(false)
89+
.message(ex.getMessage())
90+
.error(ErrorResponse.ErrorDetails.builder()
91+
.message(ex.getMessage())
92+
.code("SERVICE_UNAVAILABLE")
93+
.build())
94+
.timestamp(Instant.now().toString())
95+
.build();
96+
97+
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
98+
}
99+
100+
@ExceptionHandler(VoyageAuthException.class)
101+
public ResponseEntity<ErrorResponse> handleVoyageAuthException(
102+
VoyageAuthException ex, WebRequest request) {
103+
logger.error("Voyage AI authentication error: {}", ex.getMessage());
104+
105+
ErrorResponse errorResponse = ErrorResponse.builder()
106+
.success(false)
107+
.message(ex.getMessage())
108+
.error(ErrorResponse.ErrorDetails.builder()
109+
.message(ex.getMessage())
110+
.code("VOYAGE_AUTH_ERROR")
111+
.details("Please verify your VOYAGE_API_KEY is correct in the .env file")
112+
.build())
113+
.timestamp(Instant.now().toString())
114+
.build();
115+
116+
return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
117+
}
118+
119+
@ExceptionHandler(VoyageAPIException.class)
120+
public ResponseEntity<ErrorResponse> handleVoyageAPIException(
121+
VoyageAPIException ex, WebRequest request) {
122+
logger.error("Voyage AI API error: {}", ex.getMessage());
123+
124+
ErrorResponse errorResponse = ErrorResponse.builder()
125+
.success(false)
126+
.message("Vector search service unavailable")
127+
.error(ErrorResponse.ErrorDetails.builder()
128+
.message(ex.getMessage())
129+
.code("VOYAGE_API_ERROR")
130+
.build())
131+
.timestamp(Instant.now().toString())
132+
.build();
133+
134+
return new ResponseEntity<>(errorResponse, HttpStatus.SERVICE_UNAVAILABLE);
135+
}
136+
82137
@ExceptionHandler(DatabaseOperationException.class)
83138
public ResponseEntity<ErrorResponse> handleDatabaseOperationException(
84139
DatabaseOperationException ex, WebRequest request) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.mongodb.samplemflix.exception;
2+
3+
/**
4+
* Exception thrown when a required service is unavailable or not configured.
5+
*
6+
* This exception results in a 400 Bad Request response with SERVICE_UNAVAILABLE code.
7+
* Typically occurs when:
8+
* - A required API key is not configured
9+
* - A required service is not available
10+
*/
11+
public class ServiceUnavailableException extends RuntimeException {
12+
13+
public ServiceUnavailableException(String message) {
14+
super(message);
15+
}
16+
}
17+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.mongodb.samplemflix.exception;
2+
3+
/**
4+
* Exception thrown when Voyage AI API returns an error.
5+
*
6+
* This exception results in a 503 Service Unavailable response.
7+
* Typically occurs when:
8+
* - The Voyage AI API is down or unavailable
9+
* - The API returns an error response
10+
* - Network issues prevent communication with the API
11+
*/
12+
public class VoyageAPIException extends RuntimeException {
13+
14+
private final int statusCode;
15+
16+
public VoyageAPIException(String message, int statusCode) {
17+
super(message);
18+
this.statusCode = statusCode;
19+
}
20+
21+
public VoyageAPIException(String message) {
22+
this(message, 503);
23+
}
24+
25+
public int getStatusCode() {
26+
return statusCode;
27+
}
28+
}
29+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.mongodb.samplemflix.exception;
2+
3+
/**
4+
* Exception thrown when Voyage AI API authentication fails.
5+
*
6+
* This exception results in a 401 Unauthorized response.
7+
* Typically occurs when:
8+
* - The API key is invalid
9+
* - The API key is missing
10+
* - The API key has expired
11+
*/
12+
public class VoyageAuthException extends RuntimeException {
13+
14+
public VoyageAuthException(String message) {
15+
super(message);
16+
}
17+
}
18+

mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import com.mongodb.client.result.UpdateResult;
77
import com.mongodb.samplemflix.exception.DatabaseOperationException;
88
import com.mongodb.samplemflix.exception.ResourceNotFoundException;
9+
import com.mongodb.samplemflix.exception.ServiceUnavailableException;
910
import com.mongodb.samplemflix.exception.ValidationException;
11+
import com.mongodb.samplemflix.exception.VoyageAPIException;
12+
import com.mongodb.samplemflix.exception.VoyageAuthException;
1013
import com.mongodb.samplemflix.model.Movie;
1114
import com.mongodb.samplemflix.model.dto.*;
1215
import com.mongodb.samplemflix.repository.MovieRepository;
@@ -821,8 +824,8 @@ public List<VectorSearchResult> vectorSearchMovies(String query, Integer limit)
821824
// Check if Voyage API key is configured
822825
if (voyageApiKey == null || voyageApiKey.trim().isEmpty() ||
823826
voyageApiKey.equals("your_voyage_api_key")) {
824-
throw new ValidationException(
825-
"Vector search unavailable: VOYAGE_API_KEY not configured. Please add your Voyage AI API key to the .env file"
827+
throw new ServiceUnavailableException(
828+
"Vector search unavailable: VOYAGE_API_KEY not configured. Please add your API key to the .env file"
826829
);
827830
}
828831

@@ -929,10 +932,16 @@ public List<VectorSearchResult> vectorSearchMovies(String query, Integer limit)
929932

930933
return results;
931934

935+
} catch (VoyageAuthException e) {
936+
// Re-raise Voyage AI authentication errors to be handled by GlobalExceptionHandler
937+
throw e;
938+
} catch (VoyageAPIException e) {
939+
// Re-raise Voyage AI API errors to be handled by GlobalExceptionHandler
940+
throw e;
932941
} catch (IOException e) {
933-
// Handle Voyage AI API errors
942+
// Handle network errors calling Voyage AI API
934943
String errorMsg = e.getMessage() != null ? e.getMessage() : "Network error calling Voyage AI API";
935-
throw new DatabaseOperationException("Error performing vector search: " + errorMsg);
944+
throw new VoyageAPIException("Error performing vector search: " + errorMsg);
936945
} catch (InterruptedException e) {
937946
Thread.currentThread().interrupt();
938947
throw new DatabaseOperationException("Vector search was interrupted");
@@ -981,9 +990,12 @@ private List<Double> generateVoyageEmbedding(String text, String apiKey) throws
981990
if (response.statusCode() != 200) {
982991
// Handle authentication errors specifically
983992
if (response.statusCode() == 401) {
984-
throw new IOException("Invalid Voyage AI API key. Please check your VOYAGE_API_KEY in the .env file");
993+
throw new VoyageAuthException("Invalid Voyage AI API key. Please check your VOYAGE_API_KEY in the .env file");
985994
}
986-
throw new IOException("Voyage AI API returned status code " + response.statusCode() + ": " + response.body());
995+
throw new VoyageAPIException(
996+
"Voyage AI API returned status code " + response.statusCode() + ": " + response.body(),
997+
response.statusCode()
998+
);
987999
}
9881000

9891001
// Parse the JSON response to extract the embedding

mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.mongodb.client.MongoCollection;
99
import com.mongodb.client.result.UpdateResult;
1010
import com.mongodb.samplemflix.exception.ResourceNotFoundException;
11+
import com.mongodb.samplemflix.exception.ServiceUnavailableException;
1112
import com.mongodb.samplemflix.exception.ValidationException;
1213
import com.mongodb.samplemflix.model.Movie;
1314
import com.mongodb.samplemflix.model.dto.BatchInsertResponse;
@@ -736,23 +737,23 @@ void testVectorSearchMovies_EmptyQuery() {
736737
}
737738

738739
@Test
739-
@DisplayName("Should throw ValidationException when API key is missing in vector search")
740+
@DisplayName("Should throw ServiceUnavailableException when API key is missing in vector search")
740741
void testVectorSearchMovies_MissingApiKey() {
741742
// Arrange
742743
ReflectionTestUtils.setField(movieService, "voyageApiKey", null);
743744

744745
// Act & Assert
745-
assertThrows(ValidationException.class, () -> movieService.vectorSearchMovies("test query", 10));
746+
assertThrows(ServiceUnavailableException.class, () -> movieService.vectorSearchMovies("test query", 10));
746747
}
747748

748749
@Test
749-
@DisplayName("Should throw ValidationException when API key is placeholder value in vector search")
750+
@DisplayName("Should throw ServiceUnavailableException when API key is placeholder value in vector search")
750751
void testVectorSearchMovies_PlaceholderApiKey() {
751752
// Arrange
752753
ReflectionTestUtils.setField(movieService, "voyageApiKey", "your_voyage_api_key");
753754

754755
// Act & Assert
755-
assertThrows(ValidationException.class, () -> movieService.vectorSearchMovies("test query", 10));
756+
assertThrows(ServiceUnavailableException.class, () -> movieService.vectorSearchMovies("test query", 10));
756757
}
757758

758759
@Test
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

mflix/server/python-fastapi/main.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from contextlib import asynccontextmanager
2-
from fastapi import FastAPI
2+
from fastapi import FastAPI, Request
33
from fastapi.middleware.cors import CORSMiddleware
4+
from fastapi.responses import JSONResponse
45
from src.routers import movies
56
from src.database.mongo_client import db, get_collection
7+
from src.utils.exceptions import VoyageAuthError, VoyageAPIError
8+
from src.utils.errorResponse import create_error_response
69

710
import os
811
from dotenv import load_dotenv
@@ -136,6 +139,31 @@ async def ensure_standard_index():
136139

137140
app = FastAPI(lifespan=lifespan)
138141

142+
# Add custom exception handlers
143+
@app.exception_handler(VoyageAuthError)
144+
async def voyage_auth_error_handler(request: Request, exc: VoyageAuthError):
145+
"""Handle Voyage AI authentication errors with 401 status."""
146+
return JSONResponse(
147+
status_code=401,
148+
content=create_error_response(
149+
message=exc.message,
150+
code="VOYAGE_AUTH_ERROR",
151+
details="Please verify your VOYAGE_API_KEY is correct in the .env file"
152+
)
153+
)
154+
155+
@app.exception_handler(VoyageAPIError)
156+
async def voyage_api_error_handler(request: Request, exc: VoyageAPIError):
157+
"""Handle Voyage AI API errors with 503 status."""
158+
return JSONResponse(
159+
status_code=503,
160+
content=create_error_response(
161+
message="Vector search service unavailable",
162+
code="VOYAGE_API_ERROR",
163+
details=exc.message
164+
)
165+
)
166+
139167
# Add CORS middleware
140168
cors_origins = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:3001").split(",")
141169
app.add_middleware(

mflix/server/python-fastapi/src/routers/movies.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from fastapi import APIRouter, Query, Path, Body, HTTPException
2+
from fastapi.responses import JSONResponse
23
from src.database.mongo_client import get_collection, voyage_ai_available
34
from src.models.models import VectorSearchResult, CreateMovieRequest, Movie, SuccessResponse, UpdateMovieRequest, SearchMoviesResponse
45
from typing import Any, List, Optional
56
from src.utils.successResponse import create_success_response
7+
from src.utils.errorResponse import create_error_response
8+
from src.utils.exceptions import VoyageAuthError, VoyageAPIError
69
from bson import ObjectId, errors
710
import re
811
from bson.errors import InvalidId
912
import voyageai
13+
import os
1014

1115

1216
'''
@@ -316,11 +320,16 @@ async def vector_search_movies(
316320
Returns:
317321
SuccessResponse containing a list of movies with similarity scores
318322
"""
323+
# Check if Voyage AI API key is configured
319324
if not voyage_ai_available():
320-
raise HTTPException(
321-
status_code = 503,
322-
detail="Vector search unavailable: VOYAGE_API_KEY not configured. Please add your API key to your .env file."
325+
return JSONResponse(
326+
status_code=400,
327+
content=create_error_response(
328+
message="Vector search unavailable: VOYAGE_API_KEY not configured. Please add your API key to the .env file",
329+
code="SERVICE_UNAVAILABLE"
330+
)
323331
)
332+
324333
try:
325334
# Initialize the client here to avoid import-time errors
326335
vo = voyageai.Client()
@@ -390,10 +399,20 @@ async def vector_search_movies(
390399
f"Found {len(results)} similar movies for query: '{q}'"
391400
)
392401

402+
except VoyageAuthError:
403+
# Re-raise custom exceptions to be handled by the exception handlers
404+
raise
405+
except VoyageAPIError:
406+
# Re-raise custom exceptions to be handled by the exception handlers
407+
raise
393408
except Exception as e:
409+
# Log the error for debugging
410+
print(f"Vector search error: {str(e)}")
411+
412+
# Handle generic errors
394413
raise HTTPException(
395-
status_code = 500,
396-
detail=f"An error occurred during vector search: {str(e)}"
414+
status_code=500,
415+
detail=f"Error performing vector search: {str(e)}"
397416
)
398417

399418
"""
@@ -1348,12 +1367,30 @@ def get_embedding(data, input_type = "document", client=None):
13481367
13491368
Returns:
13501369
Vector embeddings for the given input
1370+
1371+
Raises:
1372+
VoyageAuthError: If the API key is invalid (401)
1373+
VoyageAPIError: For other API errors
13511374
"""
13521375
if client is None:
13531376
client = voyageai.Client()
13541377

1355-
embeddings = client.embed(
1356-
data, model = model, output_dimension = outputDimension, input_type = input_type
1357-
).embeddings
1358-
return embeddings[0]
1378+
try:
1379+
embeddings = client.embed(
1380+
data, model = model, output_dimension = outputDimension, input_type = input_type
1381+
).embeddings
1382+
return embeddings[0]
1383+
except Exception as e:
1384+
error_message = str(e).lower()
1385+
1386+
# Check for authentication errors
1387+
if "401" in error_message or "unauthorized" in error_message or "invalid api key" in error_message:
1388+
raise VoyageAuthError("Invalid Voyage AI API key. Please check your VOYAGE_API_KEY in the .env file")
1389+
1390+
# Check for other API errors
1391+
if "api" in error_message or "voyage" in error_message:
1392+
raise VoyageAPIError(f"Voyage AI API error: {str(e)}", 503)
1393+
1394+
# Re-raise other exceptions
1395+
raise VoyageAPIError(f"Failed to generate embedding: {str(e)}", 500)
13591396

0 commit comments

Comments
 (0)