mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
fix(core): Surface model discovery authentication failures to users (#35979)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
import { isOpenAiCustomEndpoint, listModelsForProvider, MODEL_DISCOVERY_PROVIDERS } from '../index';
|
||||
|
||||
function mockFetch(body: unknown, ok = true, status = 200) {
|
||||
@@ -207,11 +209,64 @@ describe('model-discovery', () => {
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws a descriptive error on a non-2xx response', async () => {
|
||||
const fetch = mockFetch({ error: { message: 'invalid x-api-key' } }, false, 401);
|
||||
it.each([401, 403])(
|
||||
'throws a non-reportable user error on an authentication response with status %s',
|
||||
async (status) => {
|
||||
const fetch = mockFetch(
|
||||
{ error: { message: 'provider response must not be exposed' } },
|
||||
false,
|
||||
status,
|
||||
);
|
||||
|
||||
await expect(listModelsForProvider('anthropic', { apiKey: 'bad', fetch })).rejects.toThrow(
|
||||
/anthropic.*401/i,
|
||||
const error = await listModelsForProvider('anthropic', {
|
||||
apiKey: 'bad',
|
||||
fetch,
|
||||
}).catch((error: unknown) => error);
|
||||
|
||||
expect(error).toBeInstanceOf(UserError);
|
||||
expect(error).toMatchObject({
|
||||
message:
|
||||
"Models couldn't be loaded. Check that the selected credential is valid and has the required permissions, then try again.",
|
||||
shouldReport: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps server errors reportable', async () => {
|
||||
const fetch = mockFetch({ error: { message: 'provider unavailable' } }, false, 500);
|
||||
|
||||
const error = await listModelsForProvider('anthropic', {
|
||||
apiKey: 'key',
|
||||
fetch,
|
||||
}).catch((error: unknown) => error);
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error).not.toBeInstanceOf(UserError);
|
||||
expect(error).toMatchObject({
|
||||
message:
|
||||
'Failed to list anthropic models (status 500): {"error":{"message":"provider unavailable"}}',
|
||||
});
|
||||
});
|
||||
|
||||
it('propagates network errors unchanged', async () => {
|
||||
const networkError = new Error('Network unavailable');
|
||||
const fetch = vi.fn().mockRejectedValue(networkError) as unknown as typeof globalThis.fetch;
|
||||
|
||||
await expect(listModelsForProvider('anthropic', { apiKey: 'key', fetch })).rejects.toBe(
|
||||
networkError,
|
||||
);
|
||||
});
|
||||
|
||||
it('propagates malformed response errors unchanged', async () => {
|
||||
const parseError = new SyntaxError('Unexpected token');
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => await Promise.reject(parseError),
|
||||
}) as unknown as typeof globalThis.fetch;
|
||||
|
||||
await expect(listModelsForProvider('anthropic', { apiKey: 'key', fetch })).rejects.toBe(
|
||||
parseError,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
import type { ListModelsFn, ListModelsOptions, ProviderModel } from './types';
|
||||
|
||||
/** GET a provider endpoint and parse JSON, throwing a descriptive error on non-2xx. */
|
||||
/** GET a provider endpoint and parse JSON, treating rejected credentials as user errors. */
|
||||
export async function getJson(
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
@@ -13,6 +15,13 @@ export async function getJson(
|
||||
headers: { ...headers, ...options.headers },
|
||||
});
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new UserError(
|
||||
"Models couldn't be loaded. Check that the selected credential is valid and has the required permissions, then try again.",
|
||||
{ shouldReport: false },
|
||||
);
|
||||
}
|
||||
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new Error(
|
||||
`Failed to list ${provider} models (status ${response.status})${body ? `: ${body.slice(0, 500)}` : ''}`,
|
||||
|
||||
Reference in New Issue
Block a user