canina/frontend/application/app/api/media/[...path]/route.ts
parsa aghaei b68dd29541
Some checks failed
Deploy Canina / deploy (push) Successful in 1m28s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s
fix(media): enhance media proxy routing with multi-target fallback and add uploads location to nginx
2026-08-29 13:25:48 +03:30

153 lines
5.0 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
function getCandidateUrls(filePath: string): string[] {
const set = new Set<string>();
if (process.env.INTERNAL_API_URL) {
const base = process.env.INTERNAL_API_URL.replace(/\/api\/?$/, '');
set.add(`${base}/uploads/${filePath}`);
}
if (process.env.NEXT_PUBLIC_API_URL && !process.env.NEXT_PUBLIC_API_URL.startsWith('/')) {
const base = process.env.NEXT_PUBLIC_API_URL.replace(/\/api\/?$/, '');
set.add(`${base}/uploads/${filePath}`);
}
set.add(`http://backend_prod:3000/uploads/${filePath}`);
set.add(`http://canino_backend_prod:3000/uploads/${filePath}`);
set.add(`http://backend:3000/uploads/${filePath}`);
set.add(`http://127.0.0.1:4001/uploads/${filePath}`);
set.add(`http://localhost:4001/uploads/${filePath}`);
return Array.from(set);
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const { path } = await params;
if (!path || path.length === 0) {
return new NextResponse('Bad Request: Missing file path', { status: 400 });
}
const filePath = path.map(segment => encodeURIComponent(segment)).join('/');
const candidates = getCandidateUrls(filePath);
const forwardedHeaders: HeadersInit = {};
const rangeHeader = request.headers.get('range');
if (rangeHeader) forwardedHeaders['range'] = rangeHeader;
const ifNoneMatch = request.headers.get('if-none-match');
if (ifNoneMatch) forwardedHeaders['if-none-match'] = ifNoneMatch;
const ifModifiedSince = request.headers.get('if-modified-since');
if (ifModifiedSince) forwardedHeaders['if-modified-since'] = ifModifiedSince;
let backendRes: Response | null = null;
for (const targetUrl of candidates) {
try {
const res = await fetch(targetUrl, {
method: 'GET',
headers: forwardedHeaders,
cache: 'no-store',
});
if (res.ok || res.status === 304 || res.status === 206) {
backendRes = res;
break;
}
} catch {
// Continue
}
}
if (!backendRes) {
return new NextResponse('Media file not found upstream', { status: 404 });
}
const responseHeaders = new Headers();
const contentType = backendRes.headers.get('content-type') || 'application/octet-stream';
responseHeaders.set('Content-Type', contentType);
const contentLength = backendRes.headers.get('content-length');
if (contentLength) responseHeaders.set('Content-Length', contentLength);
const contentRange = backendRes.headers.get('content-range');
if (contentRange) responseHeaders.set('Content-Range', contentRange);
const acceptRanges = backendRes.headers.get('accept-ranges') || 'bytes';
responseHeaders.set('Accept-Ranges', acceptRanges);
const etag = backendRes.headers.get('etag');
if (etag) responseHeaders.set('ETag', etag);
const lastModified = backendRes.headers.get('last-modified');
if (lastModified) responseHeaders.set('Last-Modified', lastModified);
responseHeaders.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000, immutable');
responseHeaders.set('X-Content-Type-Options', 'nosniff');
return new Response(backendRes.body, {
status: backendRes.status,
statusText: backendRes.statusText,
headers: responseHeaders,
});
} catch (err: unknown) {
console.error('[Media Proxy Error]', err);
return new NextResponse('Internal Media Proxy Error', { status: 502 });
}
}
export async function HEAD(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const { path } = await params;
if (!path || path.length === 0) {
return new NextResponse(null, { status: 400 });
}
const filePath = path.map(segment => encodeURIComponent(segment)).join('/');
const candidates = getCandidateUrls(filePath);
let backendRes: Response | null = null;
for (const targetUrl of candidates) {
try {
const res = await fetch(targetUrl, { method: 'HEAD', cache: 'no-store' });
if (res.ok || res.status === 304) {
backendRes = res;
break;
}
} catch {
// Continue
}
}
if (!backendRes) {
return new NextResponse(null, { status: 404 });
}
const responseHeaders = new Headers();
const contentType = backendRes.headers.get('content-type') || 'application/octet-stream';
responseHeaders.set('Content-Type', contentType);
const contentLength = backendRes.headers.get('content-length');
if (contentLength) responseHeaders.set('Content-Length', contentLength);
responseHeaders.set('Accept-Ranges', 'bytes');
responseHeaders.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000, immutable');
return new NextResponse(null, {
status: backendRes.status,
headers: responseHeaders,
});
} catch (err: unknown) {
console.error('[Media Proxy HEAD Error]', err);
return new NextResponse(null, { status: 502 });
}
}