diff --git a/changelog.d/fixes/9102-fix.plan.md b/changelog.d/fixes/9102-fix.plan.md new file mode 100644 index 0000000000..bfbfed5c22 --- /dev/null +++ b/changelog.d/fixes/9102-fix.plan.md @@ -0,0 +1 @@ +- fix(providers): modal.com validation returns clear error when Base URL is missing, instead of leaking "Invalid outbound URL" (#9102) \ No newline at end of file diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index e9b2cef768..8312ea58e2 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -211,15 +211,30 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi oci: validateOciProvider, sap: validateSapProvider, bedrock: validateBedrockProvider, - modal: ({ apiKey, providerSpecificData }: any) => - validateOpenAILikeProvider({ + modal: ({ apiKey, providerSpecificData }: any) => { + // Modal is bring-your-own-deploy — it requires a Base URL pointing to the user's + // OpenAI-compatible Modal app. Without it, validateOpenAILikeProvider would build an + // empty probe URL and trip parseOutboundUrl with a raw guard error ("Invalid outbound + // URL: "). Surface an actionable message instead. See #9102. + const baseUrl = (providerSpecificData?.baseUrl || "").trim(); + if (!baseUrl) { + return { + valid: false, + error: + "Modal requires a Base URL pointing to your OpenAI-compatible Modal app " + + "(e.g. https://--.modal.run/v1). " + + "Fill in the \"Base URL override\" field.", + }; + } + return validateOpenAILikeProvider({ provider: "modal", apiKey, providerSpecificData, - baseUrl: normalizeBaseUrl(providerSpecificData?.baseUrl || ""), + baseUrl: normalizeBaseUrl(baseUrl), modelId: MODAL_DEFAULT_VALIDATION_MODEL_ID, isLocal, - }), + }); + }, "nous-research": validateNousResearchProvider, poe: validatePoeProvider, clarifai: validateClarifaiProvider, diff --git a/tests/unit/probe-9102-modal-nobaseurl.test.ts b/tests/unit/probe-9102-modal-nobaseurl.test.ts new file mode 100644 index 0000000000..b99f7c24c2 --- /dev/null +++ b/tests/unit/probe-9102-modal-nobaseurl.test.ts @@ -0,0 +1,30 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); + +test("modal validation without baseUrl returns clear actionable error (not Invalid outbound URL)", async () => { + // Ensure no actual fetch ever happens — the bug is a pre-fetch URL parse failure + globalThis.fetch = async (_url: RequestInfo | URL, _init?: RequestInit) => { + throw new Error("unexpected fetch: validation should fail before any network request"); + }; + + const result = await validateProviderApiKey({ + provider: "modal", + apiKey: "ak-test:as-test", + providerSpecificData: {}, + }); + + // The bug: when baseUrl is empty, validateOpenAILikeProvider gets an empty URL, + // parseOutboundUrl throws "Invalid outbound URL: " — a raw guard message. + // The fix must return a clear actionable message mentioning Base URL. + const errorMsg = result.error || ""; + assert.ok( + !errorMsg.includes("Invalid outbound URL"), + `bug: leaked raw guard message -> ${JSON.stringify(errorMsg)}` + ); + assert.ok( + errorMsg.toLowerCase().includes("base url") || errorMsg.toLowerCase().includes("base"), + `expected error to mention Base URL, got: ${JSON.stringify(errorMsg)}` + ); +});