diff --git a/web/public/errors/404.png b/web/public/errors/404.png new file mode 100644 index 0000000..0eaf9d0 Binary files /dev/null and b/web/public/errors/404.png differ diff --git a/web/public/errors/500.png b/web/public/errors/500.png new file mode 100644 index 0000000..cd83dd8 Binary files /dev/null and b/web/public/errors/500.png differ diff --git a/web/src/app/lib/errorImageResponse.ts b/web/src/app/lib/errorImageResponse.ts new file mode 100644 index 0000000..63b0ac0 --- /dev/null +++ b/web/src/app/lib/errorImageResponse.ts @@ -0,0 +1,35 @@ +import path from 'path'; +import { promises as fs } from 'fs'; +import { NextResponse } from 'next/server'; + +export async function errorImageResponse( + statusCode: number = 500, + options?: { message?: string } +): Promise { + const file = `${statusCode}.png`; + const filePath = path.join(process.cwd(), 'public/errors', file); + + const headers: Record = { + 'Content-Type': 'image/png', + }; + if (options?.message) { + headers['X-Error-Message'] = encodeURIComponent(options.message); + } + + try { + const buffer = await fs.readFile(filePath); + headers['Content-Length'] = buffer.length.toString(); + return new NextResponse(buffer, { + status: statusCode, + headers, + }); + } catch { + const fallback = path.join(process.cwd(), 'public/errors', '500.png'); + const buffer = await fs.readFile(fallback); + headers['Content-Length'] = buffer.length.toString(); + return new NextResponse(buffer, { + status: 500, + headers, + }); + } +} \ No newline at end of file diff --git a/web/src/app/thumbnails/asset/[assetId]/route.tsx b/web/src/app/thumbnails/asset/[assetId]/route.tsx index e7232d8..7c02e75 100644 --- a/web/src/app/thumbnails/asset/[assetId]/route.tsx +++ b/web/src/app/thumbnails/asset/[assetId]/route.tsx @@ -1,8 +1,18 @@ import { NextRequest, NextResponse } from 'next/server'; +import { errorImageResponse } from '@/app/lib/errorImageResponse'; const cache = new Map(); const CACHE_TTL = 15 * 60 * 1000; +setInterval(() => { + const now = Date.now(); + for (const [key, value] of cache.entries()) { + if (value.expires <= now) { + cache.delete(key); + } + } +}, 60 * 5 * 1000); + export async function GET( request: NextRequest, context: { params: Promise<{ assetId: number }> } @@ -10,10 +20,9 @@ export async function GET( const { assetId } = await context.params; if (!assetId) { - return NextResponse.json( - { error: 'Missing asset ID' }, - { status: 400 } - ); + return errorImageResponse(400, { + message: "Missing asset ID", + }) } let finalAssetId = assetId; @@ -31,17 +40,17 @@ export async function GET( } catch { } const now = Date.now(); - const cached = cache.get(finalAssetId); + const cached = cache.get(finalAssetId); - if (cached && cached.expires > now) { - return new NextResponse(cached.buffer, { - headers: { - 'Content-Type': 'image/png', - 'Content-Length': cached.buffer.length.toString(), - 'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`, - }, - }); - } + if (cached && cached.expires > now) { + return new NextResponse(cached.buffer, { + headers: { + 'Content-Type': 'image/png', + 'Content-Length': cached.buffer.length.toString(), + 'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`, + }, + }); + } try { const response = await fetch( @@ -49,22 +58,21 @@ export async function GET( ); if (!response.ok) { - throw new Error('Failed to fetch thumbnail JSON'); + throw new Error(`Failed to fetch thumbnail JSON [${response.status}]`) } const data = await response.json(); const imageUrl = data.data[0]?.imageUrl; if (!imageUrl) { - return NextResponse.json( - { error: 'No image URL found in the response' }, - { status: 404 } - ); + return errorImageResponse(404, { + message: "No image URL found in the response", + }) } const imageResponse = await fetch(imageUrl); if (!imageResponse.ok) { - throw new Error('Failed to fetch the image'); + throw new Error(`Failed to fetch the image [${imageResponse.status}]`) } const arrayBuffer = await imageResponse.arrayBuffer(); @@ -79,10 +87,9 @@ export async function GET( 'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`, }, }); - } catch { - return NextResponse.json( - { error: 'Failed to fetch or process thumbnail' }, - { status: 500 } - ); + } catch (err) { + return errorImageResponse(500, { + message: `Failed to fetch or process thumbnail: ${err}`, + }) } } \ No newline at end of file diff --git a/web/src/app/thumbnails/maps/[mapId]/route.tsx b/web/src/app/thumbnails/maps/[mapId]/route.tsx index a923d21..a6b21d3 100644 --- a/web/src/app/thumbnails/maps/[mapId]/route.tsx +++ b/web/src/app/thumbnails/maps/[mapId]/route.tsx @@ -1,16 +1,20 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server" export async function GET( request: NextRequest, context: { params: Promise<{ mapId: string }> } ): Promise { // TODO: implement this, we need a cdn for in-game map thumbnails... + + if (!process.env.API_HOST) { + throw new Error('env variable "API_HOST" is not set') + } - const { mapId } = await context.params; + const { mapId } = await context.params - const protocol = request.headers.get("x-forwarded-proto") || "https"; - const host = request.headers.get("host"); - const origin = `${protocol}://${host}`; + const apiHost = process.env.API_HOST.replace(/\/api\/?$/, "") + const redirectPath = `/thumbnails/asset/${mapId}` + const redirectUrl = `${apiHost}${redirectPath}` - return NextResponse.redirect(`${origin}/thumbnails/asset/${mapId}`); + return NextResponse.redirect(redirectUrl) } \ No newline at end of file