fix(imaging): make avatar and badge images resilient to upstream outages
- Add persistent disk cache for rendered avatars/badges (storage/imaging) so repeats never touch the flaky local renderer and cached renders survive upstream downtime - Serve cache-first with stale-on-error; cut primary/fallback timeouts from 10s/6s to 4s/4s so failing images cannot stall pages - Avatar proxy now returns a graceful 200 silhouette instead of 502 when no renderer can produce a figure, so no broken-image glyphs appear - Badge endpoint becomes a caching proxy trying configured CDN, public Habbo CDN and local /swf copy in order, and drops the fragile IP rate limit that could blank badge streams - Route all site badge images (profile, me, badges, apply pages) through the cached proxy instead of hot-linking images.habbo.com
This commit is contained in:
1 parent
c3ff497050
commit
8a66db4ed7
12 files changed
+458
-58
No files matched your search
@@ -1,5 +1,33 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { fetchAvatarImage, parseAvatarQuery } from "@/lib/imager-upstream";
|
||||
import { AVATAR_FALLBACK_SRC } from "@/lib/imager";
|
||||
import {
|
||||
fetchAvatarImage,
|
||||
ImagerUnavailableError,
|
||||
parseAvatarQuery,
|
||||
} from "@/lib/imager-upstream";
|
||||
|
||||
// The same neutral silhouette the client fallback uses, served as a real 200
|
||||
// response when no upstream can render the figure. Images on server-rendered
|
||||
// pages therefore never show a broken-image glyph; browsers revalidate quickly
|
||||
// (`no-cache`) and pick up real renders as soon as an upstream recovers.
|
||||
const PLACEHOLDER_SVG_BYTES = new TextEncoder().encode(
|
||||
decodeURIComponent(
|
||||
AVATAR_FALLBACK_SRC.slice("data:image/svg+xml;utf8,".length),
|
||||
),
|
||||
);
|
||||
|
||||
function cacheControlFor(source: string): string {
|
||||
switch (source) {
|
||||
case "fallback":
|
||||
case "cache-stale":
|
||||
// Reuse the render briefly but revalidate the upstream soon.
|
||||
return "public, max-age=300";
|
||||
case "unavailable":
|
||||
return "no-cache, max-age=0";
|
||||
default:
|
||||
return "public, max-age=3600";
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
@@ -20,17 +48,24 @@ export async function GET(request: NextRequest) {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": String(body.length),
|
||||
// Fallback renders are served with a short TTL so the configured
|
||||
// imager is retried soon instead of being masked for hours.
|
||||
"Cache-Control":
|
||||
source === "fallback"
|
||||
? "public, max-age=300"
|
||||
: "public, max-age=3600",
|
||||
"Cache-Control": cacheControlFor(source),
|
||||
"X-Imager-Source": source,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof ImagerUnavailableError) {
|
||||
return new NextResponse(PLACEHOLDER_SVG_BYTES as unknown as BodyInit, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Content-Length": String(PLACEHOLDER_SVG_BYTES.length),
|
||||
"Cache-Control": cacheControlFor("unavailable"),
|
||||
"X-Imager-Source": "unavailable",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch avatar from upstream" },
|
||||
{ status: 502 },
|
||||
|
||||
Reference in new issue
Block a user