| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { NextRequest } from 'next/server' |
| import { rateLimit, getRateLimitHeaders } from '@/lib/security/rateLimiter' |
| import { validatePredictionRequest } from '@/lib/security/validation' |
| import { createSecureResponse, addSecurityHeaders } from '@/lib/security/headers' |
| import { getInferenceUrl } from '@/lib/server/inferenceUrl' |
| import { ZodError } from 'zod' |
|
|
| |
| |
| |
| |
| const MAX_FILE_SIZE = 10 * 1024 * 1024 |
|
|
| |
| |
| |
| |
| const ALLOWED_MIME_TYPES = [ |
| 'image/jpeg', |
| 'image/jpg', |
| 'image/png', |
| 'image/webp', |
| 'image/gif', |
| ] |
|
|
| |
| const INFERENCE_TIMEOUT_MS = 30_000 |
|
|
| |
| |
| |
| |
| |
| |
| |
| function validateFileContent(file: File): boolean { |
| |
| if (!ALLOWED_MIME_TYPES.includes(file.type)) { |
| return false |
| } |
|
|
| |
| if (file.size > MAX_FILE_SIZE) { |
| return false |
| } |
|
|
| |
| if (file.size === 0) { |
| return false |
| } |
|
|
| return true |
| } |
|
|
| export async function POST(request: NextRequest) { |
| |
| |
| |
| const rateLimitResponse = rateLimit(request, '/api/predict') |
| if (rateLimitResponse) { |
| return addSecurityHeaders(rateLimitResponse) |
| } |
|
|
| try { |
| let inferenceUrl: string |
| try { |
| inferenceUrl = getInferenceUrl() |
| } catch (error: any) { |
| console.error('Inference service misconfigured:', error) |
| return createSecureResponse( |
| { error: 'Prediction service is not configured. Please set INFERENCE_URL.' }, |
| 503 |
| ) |
| } |
|
|
| |
| |
| |
| const formData = await request.formData() |
|
|
| let validatedData |
| try { |
| validatedData = await validatePredictionRequest(formData) |
| } catch (error) { |
| |
| if (error instanceof ZodError) { |
| const errorMessages = error.issues.map((e) => e.message).join(', ') |
| return createSecureResponse( |
| { error: `Validation failed: ${errorMessages}` }, |
| 400 |
| ) |
| } |
| throw error |
| } |
|
|
| const { image, crop } = validatedData |
|
|
| |
| |
| |
| if (!validateFileContent(image)) { |
| return createSecureResponse( |
| { |
| error: 'Invalid file. Must be a valid image (JPEG, PNG, WebP, GIF) under 10MB.', |
| }, |
| 400 |
| ) |
| } |
|
|
| |
| |
| const upstreamForm = new FormData() |
| upstreamForm.append('image', image) |
| upstreamForm.append('crop', crop) |
| |
| const skipCropCheck = formData.get('skip_crop_check') |
| if (typeof skipCropCheck === 'string' && skipCropCheck) { |
| upstreamForm.append('skip_crop_check', skipCropCheck) |
| } |
|
|
| let upstream: Response |
| try { |
| upstream = await fetch(`${inferenceUrl}/predict`, { |
| method: 'POST', |
| body: upstreamForm, |
| signal: AbortSignal.timeout(INFERENCE_TIMEOUT_MS), |
| }) |
| } catch (error) { |
| |
| console.error('Inference service unreachable:', error) |
| return createSecureResponse( |
| { error: 'Prediction service unavailable. Please try again shortly.' }, |
| 503 |
| ) |
| } |
|
|
| let result: any |
| try { |
| result = await upstream.json() |
| } catch { |
| console.error('Inference service returned non-JSON, status:', upstream.status) |
| return createSecureResponse( |
| { error: 'Prediction failed. Please try again later.' }, |
| 500 |
| ) |
| } |
|
|
| if (!upstream.ok) { |
| const errorMessage: string = result?.error || 'Prediction failed' |
| const msg = errorMessage.toLowerCase() |
|
|
| |
| if ( |
| msg.includes('retake the image') || |
| msg.includes('clear plant leaf') || |
| msg.includes('appears blurry') |
| ) { |
| return createSecureResponse({ error: errorMessage }, 400) |
| } |
|
|
| |
| if (msg.includes('no trained models found') || msg.includes('model not found')) { |
| return createSecureResponse( |
| { error: 'Model not ready. Please train or install a model for this crop before running analysis.' }, |
| 503 |
| ) |
| } |
|
|
| console.error('Unhandled inference error:', upstream.status, errorMessage) |
| return createSecureResponse( |
| { error: 'Prediction failed. Please try again later.' }, |
| upstream.status >= 500 ? 500 : 400 |
| ) |
| } |
|
|
| |
| |
| const response = createSecureResponse(result, 200) |
|
|
| |
| const rateLimitHeaders = getRateLimitHeaders(request, '/api/predict') |
| Object.entries(rateLimitHeaders).forEach(([key, value]) => { |
| response.headers.set(key, value) |
| }) |
|
|
| return response |
| } catch (error: any) { |
| |
| |
| |
| console.error('Prediction error:', error) |
| return createSecureResponse( |
| { error: 'Prediction failed. Please try again later.' }, |
| 500 |
| ) |
| } |
| } |
|
|