import { NextRequest } from 'next/server'; import { createLogger } from '@/lib/logger'; import { apiError, apiSuccess } from '@/lib/server/api-response'; import { resolvePDFApiKey, resolvePDFBaseUrl } from '@/lib/server/provider-config'; import { validateUrlForSSRF } from '@/lib/server/ssrf-guard'; import { MINERU_CLOUD_DEFAULT_BASE } from '@/lib/pdf/constants'; const log = createLogger('Verify PDF Provider'); export async function POST(req: NextRequest) { let providerId: string | undefined; try { const body = await req.json(); providerId = body.providerId; const { apiKey, baseUrl } = body; if (!providerId) { return apiError('MISSING_REQUIRED_FIELD', 400, 'Provider ID is required'); } // MinerU Cloud: verify by calling the cloud API with the token if (providerId === 'mineru-cloud') { const resolvedApiKey = resolvePDFApiKey(providerId, apiKey); if (!resolvedApiKey) { return apiError('MISSING_REQUIRED_FIELD', 400, 'API Key is required for MinerU Cloud'); } const clientCloudBase = (baseUrl as string | undefined) || undefined; if (clientCloudBase && process.env.NODE_ENV === 'production') { const ssrfError = await validateUrlForSSRF(clientCloudBase); if (ssrfError) { return apiError('INVALID_URL', 403, ssrfError); } } const cloudBase = ( clientCloudBase || resolvePDFBaseUrl(providerId) || MINERU_CLOUD_DEFAULT_BASE ).replace(/\/+$/, ''); // Probe the batch endpoint with an empty body to verify auth const response = await fetch(`${cloudBase}/extract-results/batch/test-connection`, { headers: { Authorization: `Bearer ${resolvedApiKey}`, Accept: 'application/json', }, signal: AbortSignal.timeout(10000), }); // Any response (including 4xx for "batch not found") means auth + connectivity works // Only network errors or 401/403 indicate a problem if (response.status === 401 || response.status === 403) { const text = await response.text().catch(() => ''); return apiError( 'INTERNAL_ERROR', 500, `Authentication failed: ${text || response.statusText}`, ); } return apiSuccess({ message: 'Connection successful', status: response.status, }); } // Self-hosted providers: verify by connecting to the base URL const clientBaseUrl = (baseUrl as string | undefined) || undefined; if (clientBaseUrl && process.env.NODE_ENV === 'production') { const ssrfError = await validateUrlForSSRF(clientBaseUrl); if (ssrfError) { return apiError('INVALID_URL', 403, ssrfError); } } const resolvedBaseUrl = clientBaseUrl ? clientBaseUrl : resolvePDFBaseUrl(providerId, baseUrl); if (!resolvedBaseUrl) { return apiError('MISSING_REQUIRED_FIELD', 400, 'Base URL is required'); } const resolvedApiKey = clientBaseUrl ? (apiKey as string | undefined) || '' : resolvePDFApiKey(providerId, apiKey); const headers: Record = {}; if (resolvedApiKey) { headers['Authorization'] = `Bearer ${resolvedApiKey}`; } const response = await fetch(resolvedBaseUrl, { headers, signal: AbortSignal.timeout(10000), redirect: 'manual', }); if (response.status >= 300 && response.status < 400) { return apiError('REDIRECT_NOT_ALLOWED', 403, 'Redirects are not allowed'); } // MinerU's FastAPI root returns 404 (no root route), but the server is reachable. // Any HTTP response (including 404) means the server is up. return apiSuccess({ message: 'Connection successful', status: response.status, }); } catch (error) { log.error(`PDF provider verification failed [provider=${providerId ?? 'unknown'}]:`, error); let errorMessage = 'Connection failed'; if (error instanceof Error) { if (error.message.includes('ECONNREFUSED')) { errorMessage = 'Cannot connect to server, please check the Base URL'; } else if (error.message.includes('ENOTFOUND')) { errorMessage = 'Server not found, please check the Base URL'; } else if (error.message.includes('timeout') || error.name === 'TimeoutError') { errorMessage = 'Connection timed out'; } else { errorMessage = error.message; } } return apiError('INTERNAL_ERROR', 500, errorMessage); } }