From e3e0a7670cfe591928c28b19d903926cf31a7e82 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 19 Feb 2026 20:06:55 +0100 Subject: [PATCH] fix(staging): completely resolve phantom 403 imgproxy caching loops via base64, traefik routing precedence, and variable mapping --- docker-compose.yml | 24 +++++++++++++++++++----- lib/imgproxy.ts | 18 +++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6af3ab5b..be795f4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: dockerfile: Dockerfile args: NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL} - DIRECTUS_URL: ${DIRECTUS_URL} + DIRECTUS_URL: "${DIRECTUS_URL}" image: registry.infra.mintel.me/mintel/klz-cables.com:${IMAGE_TAG:-latest} restart: unless-stopped networks: @@ -32,7 +32,7 @@ services: - "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}" # Public Router (Whitelist for OG Images, Sitemaps, Health) - - "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && (PathPrefix(`/health`) || PathPrefix(`/sitemap.xml`) || PathPrefix(`/robots.txt`) || PathPrefix(`/manifest.webmanifest`) || PathPrefix(`/_img`) || PathPrefix(`/api/og`) || PathPrefix(`/de/api/og`) || PathPrefix(`/en/api/og`) || PathPrefix(`/logo-white.svg`) || PathPrefix(`/icon-white.svg`) || PathPrefix(`/opengraph-image`) || PathPrefix(`/de/opengraph-image`) || PathPrefix(`/en/opengraph-image`) || PathPrefix(`/blog/opengraph-image`) || PathPrefix(`/de/blog/opengraph-image`) || PathPrefix(`/en/blog/opengraph-image`) || PathRegexp(`^/sitemap(-[0-9]+)?\\.xml$`) || PathRegexp(`.*\\.(svg|png|jpg|jpeg|gif|webp|ico|webm|mp4|map)$`))" + - "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && (PathPrefix(`/health`) || PathPrefix(`/sitemap.xml`) || PathPrefix(`/robots.txt`) || PathPrefix(`/manifest.webmanifest`) || PathPrefix(`/api/og`) || PathPrefix(`/de/api/og`) || PathPrefix(`/en/api/og`) || PathPrefix(`/logo-white.svg`) || PathPrefix(`/icon-white.svg`) || PathPrefix(`/opengraph-image`) || PathPrefix(`/de/opengraph-image`) || PathPrefix(`/en/opengraph-image`) || PathPrefix(`/blog/opengraph-image`) || PathPrefix(`/de/blog/opengraph-image`) || PathPrefix(`/en/blog/opengraph-image`) || PathRegexp(`^/sitemap(-[0-9]+)?\\.xml$`) || PathRegexp(`.*\\.(svg|png|jpg|jpeg|gif|webp|ico|webm|mp4|map)$`))" - "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}" - "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}" - "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}" @@ -165,17 +165,31 @@ services: - "cms.klz.localhost:host-gateway" - "host.docker.internal:host-gateway" environment: - IMGPROXY_URL_MAPPING: "${IMGPROXY_URL_MAPPING:-http://klz.localhost/:http://klz-app:3000/,http://cms.klz.localhost/:http://klz-cms:8055/}" + IMGPROXY_URL_MAPPING: "${NEXT_PUBLIC_BASE_URL}:http://klz-app:3000,${DIRECTUS_URL}:http://klz-cms:8055" IMGPROXY_USE_ETAG: "true" IMGPROXY_MAX_SRC_RESOLUTION: 20 IMGPROXY_IGNORE_SSL_ERRORS: "true" - IMGPROXY_DEBUG: "true" + IMGPROXY_LOG_LEVEL: debug + IMGPROXY_ALLOW_LOCAL_NETWORKS: "true" + labels: - "traefik.enable=true" - # HTTP router (local dev) + # Existing Local HTTP Router - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.rule=Host(`img.${TRAEFIK_HOST:-klz.localhost}`)" - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.entrypoints=web" - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.service=${PROJECT_NAME:-klz}-imgproxy-svc" + + # NEW: Direct Public Staging Router for /_img (Bypasses Next.js rewrites) + # This fixes the Next.js URL-decoding bug on dynamic image proxy paths + - "traefik.http.routers.${PROJECT_NAME:-klz}-img.rule=(Host(`${TRAEFIK_HOST:-klz.localhost}`) || Host(`staging.klz-cables.com`) || Host(`testing.klz-cables.com`)) && PathPrefix(`/_img`)" + - "traefik.http.routers.${PROJECT_NAME:-klz}-img.priority=99999" + - "traefik.http.routers.${PROJECT_NAME:-klz}-img.entrypoints=websecure" + - "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls=true" + - "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-le}" + - "traefik.http.routers.${PROJECT_NAME:-klz}-img.service=${PROJECT_NAME:-klz}-imgproxy-svc" + - "traefik.http.services.${PROJECT_NAME:-klz}-imgproxy-svc.loadbalancer.server.port=8080" + - "traefik.http.routers.${PROJECT_NAME:-klz}-img.middlewares=${PROJECT_NAME:-klz}-img-strip" + - "traefik.http.middlewares.${PROJECT_NAME:-klz}-img-strip.stripprefix.prefixes=/_img" # HTTPS router (staging/prod) - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.rule=Host(`img.${TRAEFIK_HOST:-klz.localhost}`)" - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.entrypoints=${TRAEFIK_ENTRYPOINT:-web}" diff --git a/lib/imgproxy.ts b/lib/imgproxy.ts index 29dfba86..4dabe011 100644 --- a/lib/imgproxy.ts +++ b/lib/imgproxy.ts @@ -55,10 +55,18 @@ export function getImgproxyUrl(src: string, options: ImgproxyOptions = {}): stri `g:${gravity}`, ].join('/'); - // Using /unsafe/ with plain/ source URL format - // plain/ format works reliably with imgproxy URL mapping - // Format: /unsafe//plain/[@] - const suffix = extension ? `@${extension}` : ''; + // Using Base64 encoding for the source URL. + // This completely eliminates any risk of intermediate proxies (Traefik/Next.js) + // URL-decoding the path, which corrupts the double-slash (// to /) and causes 403 errors. + // Imgproxy expects URL-safe Base64 (RFC 4648) without padding. + const b64 = + typeof window === 'undefined' + ? Buffer.from(absoluteSrc).toString('base64') + : btoa(unescape(encodeURIComponent(absoluteSrc))); - return `${baseUrl}/unsafe/${processingOptions}/plain/${encodeURIComponent(absoluteSrc)}${suffix}`; + const urlSafeB64 = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + + const suffix = extension ? `.${extension}` : ''; + + return `${baseUrl}/unsafe/${processingOptions}/${urlSafeB64}${suffix}`; }