Skip to content

Commit 35d46cf

Browse files
authored
Merge pull request #50 from mongodb/docsp-55531-voyage-api-error-handling
DOCSP-55531: Add voyage api error handling
2 parents d3148f9 + 4972a4e commit 35d46cf

17 files changed

Lines changed: 446 additions & 54 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/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

0 commit comments

Comments
 (0)