Spaces:
No application file
No application file
| /** | |
| * Media Proxy API | |
| * | |
| * Server-side proxy for fetching remote media URLs (images/videos). | |
| * Required because browser fetch() to remote CDN URLs fails with CORS errors. | |
| * The media orchestrator uses this to download generated media as blobs | |
| * for IndexedDB persistence. | |
| * | |
| * POST /api/proxy-media | |
| * Body: { url: string } | |
| * Response: Binary blob with appropriate Content-Type | |
| */ | |
| import { NextRequest, NextResponse } from 'next/server'; | |
| import { validateUrlForSSRF } from '@/lib/server/ssrf-guard'; | |
| import { apiError } from '@/lib/server/api-response'; | |
| import { createLogger } from '@/lib/logger'; | |
| const log = createLogger('ProxyMedia'); | |
| export const maxDuration = 60; | |
| export async function POST(request: NextRequest) { | |
| try { | |
| const { url } = await request.json(); | |
| if (!url || typeof url !== 'string') { | |
| return apiError('MISSING_REQUIRED_FIELD', 400, 'Missing or invalid url'); | |
| } | |
| // Block local/private network URLs to prevent SSRF | |
| const ssrfError = validateUrlForSSRF(url); | |
| if (ssrfError) { | |
| return apiError('INVALID_URL', 403, ssrfError); | |
| } | |
| // Disable redirect following to prevent redirect-to-internal attacks | |
| const response = await fetch(url, { redirect: 'manual' }); | |
| if (response.status >= 300 && response.status < 400) { | |
| return apiError('REDIRECT_NOT_ALLOWED', 403, 'Redirects are not allowed'); | |
| } | |
| if (!response.ok) { | |
| return apiError('UPSTREAM_ERROR', 502, `Upstream returned ${response.status}`); | |
| } | |
| const blob = await response.blob(); | |
| const contentType = response.headers.get('content-type') || 'application/octet-stream'; | |
| return new NextResponse(blob, { | |
| headers: { | |
| 'Content-Type': contentType, | |
| 'Content-Length': String(blob.size), | |
| 'Cache-Control': 'private, max-age=3600', | |
| }, | |
| }); | |
| } catch (error) { | |
| log.error('Proxy media error:', error); | |
| return apiError('INTERNAL_ERROR', 500, error instanceof Error ? error.message : String(error)); | |
| } | |
| } | |