fix(emex): coerce proxy port env to number (boot-crash on real range)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

ConfigService.get<number>("EMEX_PROXY_PORT_START") returns the raw env
STRING; the port-pick arithmetic then string-concatenated it
(45 + "10001" = "4510001"), producing an out-of-range port that made
undici's `new URL` throw "Invalid URL" at EmexService construction —
crashing the entire API on boot.

A single-port range (823) happened to concat to a still-parseable "0823",
which masked the bug for months. It surfaced the moment the prod
EMEX_PROXY_PORT range was widened (823 -> 10001-10099) to let Q1's
per-request port rotation work: prod crash-looped until the env was
reverted. Coerce to a validated integer port (1-65535) with default
fallback so a real range is safe.

Regression test: constructing EmexService with string port env over a
real range must not throw.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:04:49 +03:00
parent a3ae90ec29
commit 26fae98246
2 changed files with 67 additions and 2 deletions

View File

@@ -0,0 +1,55 @@
import type { ConfigService } from "@nestjs/config";
import { describe, expect, it, vi } from "vitest";
import { EmexService } from "./emex.service";
// ConfigService.get(key, default) returns the raw ENV STRING (NestJS does not
// coerce <number> generics at runtime). These tests guard the regression where a
// string port range made the proxy-port arithmetic do string concat
// (45 + "10001" = "4510001"), producing an out-of-range port that crashed the
// whole app with undici "Invalid URL" at construction time.
function makeConfig(overrides: Record<string, string>): ConfigService {
return {
get: vi.fn((key: string, def?: unknown) =>
Object.hasOwn(overrides, key) ? overrides[key] : def,
),
} as unknown as ConfigService;
}
const browser = {} as never;
const redis = { set: vi.fn() } as never;
describe("EmexService proxy-port coercion (regression)", () => {
it("does not throw 'Invalid URL' when ports arrive as strings over a real range", () => {
const config = makeConfig({
EMEX_USE_PROXY: "true",
EMEX_PROXY_HOST: "74.81.81.81",
EMEX_PROXY_PORT_START: "10001",
EMEX_PROXY_PORT_END: "10099",
});
// Pre-fix: threw "Invalid URL" here (port "4510001" > 65535).
expect(() => new EmexService(config, browser, redis)).not.toThrow();
});
it("constructs with a single-port range (string env)", () => {
const config = makeConfig({
EMEX_USE_PROXY: "true",
EMEX_PROXY_PORT_START: "823",
EMEX_PROXY_PORT_END: "823",
});
expect(() => new EmexService(config, browser, redis)).not.toThrow();
});
it("falls back to a valid default when the port env is garbage", () => {
const config = makeConfig({
EMEX_USE_PROXY: "true",
EMEX_PROXY_PORT_START: "not-a-number",
EMEX_PROXY_PORT_END: "999999",
});
expect(() => new EmexService(config, browser, redis)).not.toThrow();
});
it("constructs cleanly with the proxy disabled", () => {
const config = makeConfig({ EMEX_USE_PROXY: "false" });
expect(() => new EmexService(config, browser, redis)).not.toThrow();
});
});

View File

@@ -133,12 +133,22 @@ export class EmexService {
this.emexDirectFallback =
this.configService.get<string>("EMEX_DIRECT_FALLBACK", "false") === "true";
if (useProxy) {
// ConfigService.get<number> returns the raw env STRING, not a number. Left
// un-coerced, the port-pick arithmetic does string concat — e.g.
// 45 + "10001" = "4510001", an out-of-range port → undici `new URL` throws
// "Invalid URL" and the whole app fails to boot. A single-port range (823)
// happens to concat to a still-parseable "0823", which masked this for
// months until the range was widened. Always coerce to a valid port number.
const toPort = (key: string, def: number): number => {
const n = Number(this.configService.get(key, def));
return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : def;
};
this.emexProxy = {
host: this.configService.get<string>("EMEX_PROXY_HOST", "74.81.81.81"),
user: this.configService.get<string>("EMEX_PROXY_USER", "1726bbe361918676d44e"),
pass: this.configService.get<string>("EMEX_PROXY_PASS", "f11c7b6128cc86c6"),
portStart: this.configService.get<number>("EMEX_PROXY_PORT_START", 10001),
portEnd: this.configService.get<number>("EMEX_PROXY_PORT_END", 10099),
portStart: toPort("EMEX_PROXY_PORT_START", 10001),
portEnd: toPort("EMEX_PROXY_PORT_END", 10099),
};
// Default agent for the low-stakes image-dims path; fetchEmexHtml builds a
// fresh agent per request so a flaky port can't pin every call.