Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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