fix(files): serve /f/:public_id as CORS-enabled 200 stream instead of 302 redirect to Telegram CDN
Fixes browser CORS error when frontend fetches audio via Web Audio API (decodeAudio): the 302 redirected to api.telegram.org which lacks Access-Control-Allow-Origin, blocking fetch/XHR. Now the file bytes are proxied server-side with CORS headers + proper Content-Type, so the browser stays same-origin. Also adds CORS to chunked/archive streams.
This commit is contained in:
@@ -106,7 +106,12 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
|
|||||||
return fail(501, 'Archive entry extraction is not supported for chunked files');
|
return fail(501, 'Archive entry extraction is not supported for chunked files');
|
||||||
}
|
}
|
||||||
const range = { type: 'none' as const };
|
const range = { type: 'none' as const };
|
||||||
return chunkedStorage.createChunkedObjectResponse({ file, range, reqId: '' });
|
const resp = await chunkedStorage.createChunkedObjectResponse({ file, range, reqId: '' });
|
||||||
|
// Ensure CORS so browser fetch/XHR playback works for chunked files too.
|
||||||
|
const h = new Headers(resp.headers);
|
||||||
|
if (!h.has('Access-Control-Allow-Origin')) h.set('Access-Control-Allow-Origin', '*');
|
||||||
|
h.set('Access-Control-Expose-Headers', 'Content-Disposition, Content-Length, Accept-Ranges');
|
||||||
|
return new Response(resp.body, { status: resp.status, headers: h });
|
||||||
}
|
}
|
||||||
|
|
||||||
const archiveEntryName = file.archiveEntryName;
|
const archiveEntryName = file.archiveEntryName;
|
||||||
@@ -150,19 +155,70 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
|
|||||||
'Content-Type': file.mimeType || 'application/octet-stream',
|
'Content-Type': file.mimeType || 'application/octet-stream',
|
||||||
'Content-Disposition': `attachment; filename="${sanitizeFilenameHeader(file.fileName)}"`,
|
'Content-Disposition': `attachment; filename="${sanitizeFilenameHeader(file.fileName)}"`,
|
||||||
'Content-Length': String(loc.length),
|
'Content-Length': String(loc.length),
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Expose-Headers': 'Content-Disposition, Content-Length, Accept-Ranges',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileInfo = await getTelegramFileInfo(file.telegramFileId, publicId);
|
const fileInfo = await getTelegramFileInfo(file.telegramFileId, publicId);
|
||||||
const redirectUrl = buildTelegramFileUrl(fileInfo.file_path, fileInfo.bot_token);
|
const telegramUrl = buildTelegramFileUrl(fileInfo.file_path, fileInfo.bot_token);
|
||||||
|
|
||||||
return new Response(null, {
|
// CORS headers shared across all delivery modes — public file CDN.
|
||||||
status: 302,
|
const corsHeaders: Record<string, string> = {
|
||||||
headers: {
|
'Access-Control-Allow-Origin': '*',
|
||||||
Location: redirectUrl,
|
'Access-Control-Expose-Headers': 'Content-Disposition, Content-Length, Accept-Ranges',
|
||||||
},
|
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
|
||||||
});
|
'Access-Control-Allow-Headers': 'Range, Content-Type',
|
||||||
|
Vary: 'Origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(telegramUrl);
|
||||||
|
if (!upstream.ok) {
|
||||||
|
logger.error('Telegram file download failed', {
|
||||||
|
publicId,
|
||||||
|
status: upstream.status,
|
||||||
|
});
|
||||||
|
return Response.json(
|
||||||
|
{ error: 'Upstream download failed' },
|
||||||
|
{
|
||||||
|
status: upstream.status === 404 ? 404 : 502,
|
||||||
|
headers: corsHeaders,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstreamHeaders = new Headers(upstream.headers);
|
||||||
|
const contentType =
|
||||||
|
file.mimeType || upstreamHeaders.get('content-type') || 'application/octet-stream';
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Content-Disposition': `inline; filename="${sanitizeFilenameHeader(file.fileName)}"`,
|
||||||
|
'Content-Length': String(file.sizeBytes ?? 0),
|
||||||
|
'Cache-Control': 'public, max-age=300',
|
||||||
|
...corsHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stream the Telegram CDN body back to the client (no cross-origin hop
|
||||||
|
// in the browser → no CORS block for fetch/XHR audio playback).
|
||||||
|
return new Response(upstream.body, {
|
||||||
|
status: 200,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error('Telegram file proxy error', {
|
||||||
|
publicId,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
return Response.json(
|
||||||
|
{ error: 'Upstream download failed' },
|
||||||
|
{
|
||||||
|
status: 502,
|
||||||
|
headers: corsHeaders,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('File redirect error', { publicId, error: getErrorMessage(error) });
|
logger.error('File redirect error', { publicId, error: getErrorMessage(error) });
|
||||||
return fail(500, 'Server error');
|
return fail(500, 'Server error');
|
||||||
|
|||||||
Reference in New Issue
Block a user