Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mflix/client/app/aggregations/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '../lib/api';
import { MovieWithComments, YearlyStats, DirectorStats } from '../types/aggregations';
import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '@/lib/api';
import { MovieWithComments, YearlyStats, DirectorStats } from '@/types/aggregations';
import styles from './aggregations.module.css';

export default async function AggregationsPage() {
Expand Down
32 changes: 26 additions & 6 deletions mflix/client/app/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,16 +680,36 @@ export async function vectorSearchMovies(searchParams: {
const result = await response.json();

if (!response.ok) {
return {
success: false,
error: result.error || `Failed to perform vector search: ${response.status}`
// Extract error message from the standardized error response
const errorMessage = result.message || result.error?.message || `Failed to perform vector search: ${response.status}`;
const errorCode = result.error?.code;

// Provide user-friendly messages for specific error codes
if (errorCode === 'VOYAGE_AUTH_ERROR') {
return {
success: false,
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.'
};
}

if (errorCode === 'SERVICE_UNAVAILABLE' || errorCode === 'VOYAGE_API_ERROR') {
return {
success: false,
error: errorMessage || 'Vector search service is currently unavailable. Please try again later.'
};
}

return {
success: false,
error: errorMessage
};
}

if (!result.success) {
return {
success: false,
error: result.error || 'API returned error response'
const errorMessage = result.message || result.error?.message || 'API returned error response';
return {
success: false,
error: errorMessage
};
}

Expand Down
2 changes: 1 addition & 1 deletion mflix/client/app/movie/[id]/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect } from 'react';
import Link from 'next/link';
import { ROUTES } from '../../lib/constants';
import { ROUTES } from '@/lib/constants';
import styles from './error.module.css';

export default function MovieDetailsError({
Expand Down
2 changes: 1 addition & 1 deletion mflix/client/app/movie/[id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pageStyles from './page.module.css';
import { MovieDetailsSkeleton } from '../../components/LoadingSkeleton';
import { MovieDetailsSkeleton } from '@/components';

export default function MovieDetailsLoading() {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,61 @@ public ResponseEntity<ErrorResponse> handleMissingServletRequestParameter(
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(ServiceUnavailableException.class)
public ResponseEntity<ErrorResponse> handleServiceUnavailableException(
ServiceUnavailableException ex, WebRequest request) {
logger.error("Service unavailable: {}", ex.getMessage());

ErrorResponse errorResponse = ErrorResponse.builder()
.success(false)
.message(ex.getMessage())
.error(ErrorResponse.ErrorDetails.builder()
.message(ex.getMessage())
.code("SERVICE_UNAVAILABLE")
.build())
.timestamp(Instant.now().toString())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(VoyageAuthException.class)
public ResponseEntity<ErrorResponse> handleVoyageAuthException(
VoyageAuthException ex, WebRequest request) {
logger.error("Voyage AI authentication error: {}", ex.getMessage());

ErrorResponse errorResponse = ErrorResponse.builder()
.success(false)
.message(ex.getMessage())
.error(ErrorResponse.ErrorDetails.builder()
.message(ex.getMessage())
.code("VOYAGE_AUTH_ERROR")
.details("Please verify your VOYAGE_API_KEY is correct in the .env file")
.build())
.timestamp(Instant.now().toString())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
}

@ExceptionHandler(VoyageAPIException.class)
public ResponseEntity<ErrorResponse> handleVoyageAPIException(
VoyageAPIException ex, WebRequest request) {
logger.error("Voyage AI API error: {}", ex.getMessage());

ErrorResponse errorResponse = ErrorResponse.builder()
.success(false)
.message("Vector search service unavailable")
.error(ErrorResponse.ErrorDetails.builder()
.message(ex.getMessage())
.code("VOYAGE_API_ERROR")
.build())
.timestamp(Instant.now().toString())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.SERVICE_UNAVAILABLE);
}

@ExceptionHandler(DatabaseOperationException.class)
public ResponseEntity<ErrorResponse> handleDatabaseOperationException(
DatabaseOperationException ex, WebRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mongodb.samplemflix.exception;

/**
* Exception thrown when a required service is unavailable or not configured.
*
* This exception results in a 400 Bad Request response with SERVICE_UNAVAILABLE code.
* Typically occurs when:
* - A required API key is not configured
* - A required service is not available
*/
public class ServiceUnavailableException extends RuntimeException {

public ServiceUnavailableException(String message) {
super(message);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.mongodb.samplemflix.exception;

/**
* Exception thrown when Voyage AI API returns an error.
*
* This exception results in a 503 Service Unavailable response.
* Typically occurs when:
* - The Voyage AI API is down or unavailable
* - The API returns an error response
* - Network issues prevent communication with the API
*/
public class VoyageAPIException extends RuntimeException {

private final int statusCode;

public VoyageAPIException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}

public VoyageAPIException(String message) {
this(message, 503);
}

public int getStatusCode() {
return statusCode;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.mongodb.samplemflix.exception;

/**
* Exception thrown when Voyage AI API authentication fails.
*
* This exception results in a 401 Unauthorized response.
* Typically occurs when:
* - The API key is invalid
* - The API key is missing
* - The API key has expired
*/
public class VoyageAuthException extends RuntimeException {

public VoyageAuthException(String message) {
super(message);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import com.mongodb.client.result.UpdateResult;
import com.mongodb.samplemflix.exception.DatabaseOperationException;
import com.mongodb.samplemflix.exception.ResourceNotFoundException;
import com.mongodb.samplemflix.exception.ServiceUnavailableException;
import com.mongodb.samplemflix.exception.ValidationException;
import com.mongodb.samplemflix.exception.VoyageAPIException;
import com.mongodb.samplemflix.exception.VoyageAuthException;
import com.mongodb.samplemflix.model.Movie;
import com.mongodb.samplemflix.model.dto.*;
import com.mongodb.samplemflix.repository.MovieRepository;
Expand Down Expand Up @@ -821,8 +824,8 @@ public List<VectorSearchResult> vectorSearchMovies(String query, Integer limit)
// Check if Voyage API key is configured
if (voyageApiKey == null || voyageApiKey.trim().isEmpty() ||
voyageApiKey.equals("your_voyage_api_key")) {
throw new ValidationException(
"Vector search unavailable: VOYAGE_API_KEY not configured. Please add your Voyage AI API key to the .env file"
throw new ServiceUnavailableException(
"Vector search unavailable: VOYAGE_API_KEY not configured. Please add your API key to the .env file"
);
}

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

return results;

} catch (VoyageAuthException e) {
// Re-raise Voyage AI authentication errors to be handled by GlobalExceptionHandler
throw e;
} catch (VoyageAPIException e) {
// Re-raise Voyage AI API errors to be handled by GlobalExceptionHandler
throw e;
} catch (IOException e) {
// Handle Voyage AI API errors
// Handle network errors calling Voyage AI API
String errorMsg = e.getMessage() != null ? e.getMessage() : "Network error calling Voyage AI API";
throw new DatabaseOperationException("Error performing vector search: " + errorMsg);
throw new VoyageAPIException("Error performing vector search: " + errorMsg);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new DatabaseOperationException("Vector search was interrupted");
Expand Down Expand Up @@ -981,9 +990,12 @@ private List<Double> generateVoyageEmbedding(String text, String apiKey) throws
if (response.statusCode() != 200) {
// Handle authentication errors specifically
if (response.statusCode() == 401) {
throw new IOException("Invalid Voyage AI API key. Please check your VOYAGE_API_KEY in the .env file");
throw new VoyageAuthException("Invalid Voyage AI API key. Please check your VOYAGE_API_KEY in the .env file");
}
throw new IOException("Voyage AI API returned status code " + response.statusCode() + ": " + response.body());
throw new VoyageAPIException(
"Voyage AI API returned status code " + response.statusCode() + ": " + response.body(),
response.statusCode()
);
}

// Parse the JSON response to extract the embedding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.samplemflix.exception.ResourceNotFoundException;
import com.mongodb.samplemflix.exception.ServiceUnavailableException;
import com.mongodb.samplemflix.exception.ValidationException;
import com.mongodb.samplemflix.model.Movie;
import com.mongodb.samplemflix.model.dto.BatchInsertResponse;
Expand Down Expand Up @@ -736,23 +737,23 @@ void testVectorSearchMovies_EmptyQuery() {
}

@Test
@DisplayName("Should throw ValidationException when API key is missing in vector search")
@DisplayName("Should throw ServiceUnavailableException when API key is missing in vector search")
void testVectorSearchMovies_MissingApiKey() {
// Arrange
ReflectionTestUtils.setField(movieService, "voyageApiKey", null);

// Act & Assert
assertThrows(ValidationException.class, () -> movieService.vectorSearchMovies("test query", 10));
assertThrows(ServiceUnavailableException.class, () -> movieService.vectorSearchMovies("test query", 10));
}

@Test
@DisplayName("Should throw ValidationException when API key is placeholder value in vector search")
@DisplayName("Should throw ServiceUnavailableException when API key is placeholder value in vector search")
void testVectorSearchMovies_PlaceholderApiKey() {
// Arrange
ReflectionTestUtils.setField(movieService, "voyageApiKey", "your_voyage_api_key");

// Act & Assert
assertThrows(ValidationException.class, () -> movieService.vectorSearchMovies("test query", 10));
assertThrows(ServiceUnavailableException.class, () -> movieService.vectorSearchMovies("test query", 10));
}

@Test
Expand Down
Loading