From 2e2c5c09ce29e0deb243e9c441bd026b9f727ebb Mon Sep 17 00:00:00 2001 From: gnezim Date: Sat, 18 Apr 2026 23:56:28 +0300 Subject: [PATCH] Fix __ENV__ truncation; route API_BASE_URL through the same injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps blocked http://flights-ui.devwebzavod.ru/ru/flights-map: 1. The inline was written with the Leaflet tile template ('/map/api/tile/{z}/{x}/{y}.jpeg') embedded directly. Rspack's html-plugin pre-processes the children string and ate the '{z}' placeholder, truncating the injected JS literal to '/map/api/tile/{z' — MAP_TILE_URL on the client ended up broken and getEnv() fell back to the default. Escape every '{'/'}' inside the stringified value as '\u007B'/'\u007D'. JS decodes the Unicode escapes back to '{}' at parse time; the html plugin's template engine sees no placeholders to eat. Object-literal braces outside the string stay raw (Unicode escapes aren't valid in operator positions in JS source). 2. API_BASE_URL was still hard-defaulting to 'http://localhost:8080/api', so every dictionary fetch on the deployed cluster died with ERR_CONNECTION_REFUSED. Thread API_BASE_URL through the same PUBLIC_ENV_KEYS/html.tags path as MAP_TILE_URL, add matching Docker ARG/ENV, and forward it in deployment/build-docker.sh + k8s manifest. The devwebzavod default for both is https://flights.test.aeroflot.ru — where the real Aeroflot ingress terminates /map/api/** and /api/**. Prod keeps overriding with same-origin URLs. --- Dockerfile.react | 2 ++ deployment/build-docker.sh | 17 ++++++++++------- deployment/k8s/flights-ui.yaml | 13 ++++++++----- modern.config.ts | 20 +++++++++++++++++--- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/Dockerfile.react b/Dockerfile.react index f160165f..48aba330 100644 --- a/Dockerfile.react +++ b/Dockerfile.react @@ -26,6 +26,8 @@ COPY config/ config/ # into the image if the cluster can't set env at runtime. ARG MAP_TILE_URL ENV MAP_TILE_URL=${MAP_TILE_URL} +ARG API_BASE_URL +ENV API_BASE_URL=${API_BASE_URL} RUN pnpm build:standalone diff --git a/deployment/build-docker.sh b/deployment/build-docker.sh index 59d877df..8a1844f4 100644 --- a/deployment/build-docker.sh +++ b/deployment/build-docker.sh @@ -10,17 +10,20 @@ function build { export CopyRetryCount=100 docker rmi -f $(docker images aeroflot.$K8NAMESPACE/$1 -a -q|uniq) >/dev/null 2>/dev/null ls -l "$2/Dockerfile" - # MAP_TILE_URL must be a build-arg (not just a runtime env) because - # Modern.js bakes html.tags into the HTML template at `pnpm build:standalone` - # time. Setting it only on the pod won't change the served HTML. - # Defaults here keep the prod-cluster behavior (same-origin) intact; - # devwebzavod builds override with the flights.test.aeroflot.ru URL. - : "${MAP_TILE_URL:=/map/api/tile/{z}/{x}/{y}.jpeg}" - export MAP_TILE_URL + # MAP_TILE_URL / API_BASE_URL must flow as build-args (not just runtime + # env) because Modern.js bakes html.tags into the HTML template at + # `pnpm build:standalone` time. Values set only on the pod don't change + # the served HTML. The defaults match the devwebzavod cluster (no + # /map/api/** or /api/** ingress rule — hit the upstream that the real + # Aeroflot ingress terminates). Prod overrides with same-origin. + : "${MAP_TILE_URL:=https://flights.test.aeroflot.ru/map/api/tile/{z}/{x}/{y}.jpeg}" + : "${API_BASE_URL:=https://flights.test.aeroflot.ru/api}" + export MAP_TILE_URL API_BASE_URL docker build -t aeroflot.$K8NAMESPACE/$1:v$VERSION.$BUILD_NUMBER \ --build-arg ENVIRONMENT=${ENVIRONMENT} \ --build-arg=NPM_PROXY \ --build-arg MAP_TILE_URL=${MAP_TILE_URL} \ + --build-arg API_BASE_URL=${API_BASE_URL} \ -f "$2/Dockerfile" $2 } diff --git a/deployment/k8s/flights-ui.yaml b/deployment/k8s/flights-ui.yaml index e31c21aa..2df82e1e 100644 --- a/deployment/k8s/flights-ui.yaml +++ b/deployment/k8s/flights-ui.yaml @@ -30,11 +30,14 @@ spec: value: production - name: ENVIRONMENT value: $ENVIRONMENT - # flights-ui.devwebzavod.ru has no /map/api/** ingress rule, so - # point Leaflet at the upstream tile service that the real - # Aeroflot ingress serves (flights.test.aeroflot.ru terminates - # /map/api/** correctly). Override per-cluster if a different - # tile source is required. + # flights-ui.devwebzavod.ru has no /map/api/** or /api/** ingress + # rule, so point Leaflet and the API client at the upstream + # services that the real Aeroflot ingress terminates. + # Both MAP_TILE_URL and API_BASE_URL are also wired as docker + # build-args because Modern.js bakes html.tags into the HTML at + # build time; the k8s env still flows to the Node SSR process. - name: MAP_TILE_URL value: https://flights.test.aeroflot.ru/map/api/tile/{z}/{x}/{y}.jpeg + - name: API_BASE_URL + value: https://flights.test.aeroflot.ru/api $IMAGE_PULL_SECRETS diff --git a/modern.config.ts b/modern.config.ts index 8501e106..fff91cb6 100644 --- a/modern.config.ts +++ b/modern.config.ts @@ -9,15 +9,29 @@ const isRemote = buildTarget === "remote"; // any value we want per-deployment (e.g. MAP_TILE_URL pointing at a tile // service that the local ingress doesn't proxy) has to be injected into // the HTML as a window global. getEnv() picks up window.__ENV__ on the -// client. Evaluated once at Modern.js server start → pod env → HTML. -const PUBLIC_ENV_KEYS = ["MAP_TILE_URL"] as const; +// client. Evaluated once at Modern.js build time → pod env → HTML. +// +// IMPORTANT: Rspack's HTML plugin passes `children` through a template +// engine that eats `{token}` pairs (it was swallowing `{z}`, `{x}`, `{y}` +// from the Leaflet tile URL, leaving "/map/api/tile/{z"). Escape every +// brace INSIDE string values as `\u007B` / `\u007D`; JS decodes those +// back to `{` / `}` at parse time but the HTML preprocessor sees no +// placeholders to substitute. Object-literal braces outside strings stay +// raw because Unicode escapes aren't valid in operator positions in JS +// source. +const PUBLIC_ENV_KEYS = ["MAP_TILE_URL", "API_BASE_URL"] as const; const PUBLIC_ENV: Record = {}; for (const k of PUBLIC_ENV_KEYS) { const v = process.env[k]; if (typeof v === "string" && v.length > 0) PUBLIC_ENV[k] = v; } +const escapeInString = (s: string): string => + s.replace(/\{/g, "\\u007B").replace(/\}/g, "\\u007D"); +const pairs = Object.entries(PUBLIC_ENV) + .map(([k, v]) => `${JSON.stringify(k)}:${escapeInString(JSON.stringify(v))}`) + .join(","); const PUBLIC_ENV_SCRIPT = - `window.__ENV__ = Object.assign(window.__ENV__ || {}, ${JSON.stringify(PUBLIC_ENV)});`; + `window.__ENV__=Object.assign(window.__ENV__||{},{${pairs}});`; export default defineConfig({ plugins: [appTools({ bundler: "rspack" }), moduleFederationPlugin()],