diff --git a/src/lib/onboard/inference-selection-validation.test.ts b/src/lib/onboard/inference-selection-validation.test.ts index bd6f5a0a350..e2c3263033b 100644 --- a/src/lib/onboard/inference-selection-validation.test.ts +++ b/src/lib/onboard/inference-selection-validation.test.ts @@ -240,11 +240,7 @@ describe("inference selection validation", () => { it("routes an unreachable custom endpoint through transport recovery, not a silent loop (#6854)", async () => { const probeOpenAiLikeEndpoint = vi.fn(() => ({ ok: true, api: "openai-completions" })); - let capturedRecovery: { kind?: string } | undefined; - const promptValidationRecovery = vi.fn(async (_label: string, recovery: { kind?: string }) => { - capturedRecovery = recovery; - return "retry" as const; - }); + const promptValidationRecovery = vi.fn(async () => "retry" as const); const error = vi.spyOn(console, "error").mockImplementation(() => {}); const helpers = createInferenceSelectionValidationHelpers({ isNonInteractive: () => false, @@ -258,18 +254,21 @@ describe("inference selection validation", () => { }); try { - await helpers.validateCustomOpenAiLikeSelection( + await expect( + helpers.validateCustomOpenAiLikeSelection( + "Custom endpoint", + "https://example.invalid/v1", + "model-a", + "COMPATIBLE_API_KEY", + ), + ).resolves.toEqual({ ok: false, retry: "retry" }); + expect(probeOpenAiLikeEndpoint).not.toHaveBeenCalled(); + expect(promptValidationRecovery).toHaveBeenCalledWith( "Custom endpoint", - "https://example.invalid/v1", - "model-a", + expect.objectContaining({ kind: "transport", retry: "retry" }), "COMPATIBLE_API_KEY", + null, ); - // A DNS-unreachable endpoint is a transport failure: the recovery prompt - // receives a transport classification (DNS/VPN/URL hint + retry/back/exit), - // not the silent selection loop the private-IP path takes. - expect(promptValidationRecovery).toHaveBeenCalled(); - expect(capturedRecovery?.kind).toBe("transport"); - expect(probeOpenAiLikeEndpoint).not.toHaveBeenCalled(); } finally { error.mockRestore(); } diff --git a/src/lib/validation-recovery.test.ts b/src/lib/validation-recovery.test.ts index cae2d6e5159..c90d9cdcfba 100644 --- a/src/lib/validation-recovery.test.ts +++ b/src/lib/validation-recovery.test.ts @@ -19,6 +19,11 @@ describe("validation-recovery helpers", () => { expect( getTransportRecoveryMessage({ curlStatus: 6, message: "Could not resolve host" }), ).toContain("could not resolve"); + expect( + getTransportRecoveryMessage({ + message: 'cannot resolve endpoint host "example.invalid": getaddrinfo ENOTFOUND', + }), + ).toContain("Check DNS, VPN, or the endpoint URL"); expect( getTransportRecoveryMessage({ curlStatus: 60, message: "SSL certificate problem" }), ).toContain("TLS/certificate"); @@ -44,6 +49,20 @@ describe("validation-recovery helpers", () => { }); }); + it("routes DNS-backed preflight resolution failures through transport recovery (#6854)", () => { + const failure = { + name: "SSRF preflight", + httpStatus: 0, + curlStatus: 0, + message: 'cannot resolve endpoint host "example.invalid": getaddrinfo ENOTFOUND', + }; + expect(getProbeRecovery({ failures: [failure] })).toEqual({ + kind: "transport", + retry: "retry", + failure, + }); + }); + it("only allows model-specific retry when explicitly enabled", () => { const probe = { failures: [{ httpStatus: 400, message: "unknown model" }] }; expect(getProbeRecovery(probe)).toEqual({ kind: "unknown", retry: "selection" }); diff --git a/src/lib/validation-recovery.ts b/src/lib/validation-recovery.ts index f82a1dea17b..b93946f64c9 100644 --- a/src/lib/validation-recovery.ts +++ b/src/lib/validation-recovery.ts @@ -32,7 +32,12 @@ export function getTransportRecoveryMessage(failure: ValidationFailureLike = {}) if (failure.httpStatus && failure.httpStatus >= 500 && failure.httpStatus < 600) { return " The provider endpoint is reachable but currently failing upstream."; } - if (failure.curlStatus === 6 || /could not resolve host|name or service not known/.test(text)) { + if ( + failure.curlStatus === 6 || + /cannot resolve endpoint host|did not resolve to any address|could not resolve host|name or service not known|enotfound|eai_again/.test( + text, + ) + ) { return " Validation could not resolve the provider hostname. Check DNS, VPN, or the endpoint URL."; } if (failure.curlStatus === 7 || /connection refused|failed to connect/.test(text)) { diff --git a/src/lib/validation.ts b/src/lib/validation.ts index bfca2559b73..a5c468737f8 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -86,6 +86,13 @@ export function classifyValidationFailure({ if (/unauthorized|forbidden|invalid api key|invalid_auth|permission/i.test(normalized)) { return { kind: "credential", retry: "credential" }; } + if ( + /cannot resolve endpoint host|did not resolve to any address|could not resolve host|name or service not known|enotfound|eai_again|no http response/i.test( + normalized, + ) + ) { + return { kind: "transport", retry: "retry" }; + } if (/ssl|tls|certificate|handshake/i.test(normalized)) { return { kind: "transport", retry: "retry" }; }