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
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:
55
apps/api/src/integrations/emex/emex.service.spec.ts
Normal file
55
apps/api/src/integrations/emex/emex.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user