diff --git a/.env.example b/.env.example index b943ae2..82771d8 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ NEXT_PUBLIC_SITE_URL=https://backoffice.ghabilee.ir NEXT_PUBLIC_API_URL= NEXT_PUBLIC_FILE_SERVER_URL= +# Dev-only same-origin proxy to Nest. Leave empty in Docker/production. +# Never use http://127.0.0.1:3000 / localhost here in a container — that is this +# Next process and bakes a self-proxy CPU loop into the image at build time. API_PROXY_TARGET= diff --git a/next.config.js b/next.config.js index ace8c32..2f96b27 100644 --- a/next.config.js +++ b/next.config.js @@ -14,8 +14,36 @@ const isProduction = process.env.NODE_ENV === 'production' * local HTTP). Rewriting local HTTP Next to production Nest can therefore * emit a Secure `__Host-` cookie the browser may reject. Prefer local Nest * when testing login/refresh. See docs/reference/environment-variables.md. + * + * Never point API_PROXY_TARGET at loopback in Docker/production: inside the + * container 127.0.0.1:3000 is this Next process, so /api and /chat rewrites + * become a self-proxy CPU loop. Leave empty when the browser uses a public + * NEXT_PUBLIC_API_URL, or use the backend service hostname on a shared network. */ -const apiProxyTarget = (process.env.API_PROXY_TARGET || '').replace(/\/+$/, '') +const resolveApiProxyTarget = (raw) => { + const trimmed = (raw || '').trim().replace(/\/+$/, '') + if (!trimmed) return '' + + let hostname = '' + try { + hostname = new URL(trimmed).hostname + } catch { + console.warn(`[next.config] Ignoring invalid API_PROXY_TARGET=${JSON.stringify(raw)}`) + return '' + } + + const isLoopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' + if (isLoopback) { + console.warn( + `[next.config] Ignoring loopback API_PROXY_TARGET=${trimmed} (points at this Next process in Docker and causes a CPU loop)` + ) + return '' + } + + return trimmed +} + +const apiProxyTarget = resolveApiProxyTarget(process.env.API_PROXY_TARGET) /** Path prefix for reverse-proxy deploys (e.g. `/ghabilee`). Empty = site root. */ const rawBasePath = (process.env.NEXT_PUBLIC_BASE_PATH || '').trim() diff --git a/scripts/ci-build-image.sh b/scripts/ci-build-image.sh index 57c8405..ca68753 100755 --- a/scripts/ci-build-image.sh +++ b/scripts/ci-build-image.sh @@ -25,6 +25,21 @@ env_or_empty() { printf '%s' "${!key-}" } +# Loopback targets bake self-proxy rewrites into the image and peg a CPU core in Docker. +sanitize_api_proxy_target() { + local target + target="$(env_or_empty API_PROXY_TARGET)" + case "$target" in + *127.0.0.1*|*localhost*|*'::1'*) + echo "Warning: refusing loopback API_PROXY_TARGET for production image build" >&2 + printf '' + ;; + *) + printf '%s' "$target" + ;; + esac +} + BUILD_ARGS=( --build-arg "NEXT_PUBLIC_API_URL=$(env_or_empty NEXT_PUBLIC_API_URL)" --build-arg "NEXT_PUBLIC_FILE_SERVER_URL=$(env_or_empty NEXT_PUBLIC_FILE_SERVER_URL)" @@ -41,7 +56,7 @@ BUILD_ARGS=( --build-arg "SENTRY_AUTH_TOKEN=$(env_or_empty SENTRY_AUTH_TOKEN)" --build-arg "SENTRY_ORG=$(env_or_empty SENTRY_ORG)" --build-arg "SENTRY_PROJECT=$(env_or_empty SENTRY_PROJECT)" - --build-arg "API_PROXY_TARGET=$(env_or_empty API_PROXY_TARGET)" + --build-arg "API_PROXY_TARGET=$(sanitize_api_proxy_target)" ) echo "Building ${IMAGE_REPO}:${IMAGE_TAG} (build arguments redacted)"