Compare commits

...
Author SHA1 Message Date
Félix MalfaitandClaude Opus 4.6 50a1a777e1 fix: AI chat not showing credits exhausted or API key errors to user
Root cause: AgentChatResolver was missing @UseInterceptors, so domain
exceptions were never converted to typed GraphQL errors. They fell through
to generateGraphQLErrorFromError which created a BaseGraphQLError from
error.message (a string), losing the CustomException's subCode and
userFriendlyMessage.

Changes:
- generate-graphql-error-from-error.util.ts: Pass CustomException directly
  to BaseGraphQLError constructor (which already handles it properly)
  instead of extracting message as string and duck-typing fields after
- agent-chat.resolver.ts: Add @UseInterceptors(AgentGraphqlApiExceptionInterceptor)
- agent-graphql-api-exception-handler.util.ts: Map API_KEY_NOT_CONFIGURED
  to ForbiddenError instead of re-throwing raw
- useAgentChat.ts: Catch block now normalizes mutation errors and sets
  the error atom (same pattern as stream error handler)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 13:18:26 +02:00
4 changed files with 44 additions and 14 deletions
@@ -1,3 +1,4 @@
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useApolloClient } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback, useState } from 'react';
@@ -18,6 +19,7 @@ import {
import { agentChatInputState } from '@/ai/states/agentChatInputState';
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
import { agentChatErrorComponentFamilyState } from '@/ai/states/agentChatErrorComponentFamilyState';
import { agentChatMessagesComponentFamilyState } from '@/ai/states/agentChatMessagesComponentFamilyState';
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext';
@@ -110,6 +112,13 @@ export const useAgentChat = (
familyKey: { threadId },
});
const errorAtom = agentChatErrorComponentFamilyState.atomFamily({
instanceId: AGENT_CHAT_INSTANCE_ID,
familyKey: { threadId },
});
store.set(errorAtom, null);
const currentMessages = store.get(messagesAtom);
store.set(messagesAtom, [...currentMessages, optimisticUserMessage]);
@@ -155,7 +164,7 @@ export const useAgentChat = (
return null;
});
} catch {
} catch (error) {
setAgentChatInput(contentToSend);
setAgentChatDraftsByThreadId((prev) => ({
...prev,
@@ -168,6 +177,17 @@ export const useAgentChat = (
messagesAtom,
latestMessages.filter((message) => message.id !== messageId),
);
if (CombinedGraphQLErrors.is(error)) {
const subCode = error.errors[0]?.extensions?.subCode;
const mutationError = new Error(error.message) as Error & {
code?: string;
};
mutationError.code =
typeof subCode === 'string' ? subCode : undefined;
store.set(errorAtom, mutationError);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@@ -1,6 +1,6 @@
import { HttpException } from '@nestjs/common';
import { type I18n, type MessageDescriptor } from '@lingui/core';
import { type I18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import {
@@ -8,22 +8,29 @@ import {
ErrorCode,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { convertExceptionToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
import { CustomException } from 'src/utils/custom-exception';
export const generateGraphQLErrorFromError = (error: Error, i18n: I18n) => {
const graphqlError =
error instanceof HttpException
? convertExceptionToGraphQLError(error)
: new BaseGraphQLError(error.message, ErrorCode.INTERNAL_SERVER_ERROR);
let graphqlError: BaseGraphQLError;
if (error instanceof HttpException) {
graphqlError = convertExceptionToGraphQLError(error);
} else if (error instanceof CustomException) {
graphqlError = new BaseGraphQLError(
error,
ErrorCode.INTERNAL_SERVER_ERROR,
);
} else {
graphqlError = new BaseGraphQLError(
error.message,
ErrorCode.INTERNAL_SERVER_ERROR,
);
}
const defaultErrorMessage = msg`An error occurred.`;
const userFriendlyMessage =
'userFriendlyMessage' in error
? (error.userFriendlyMessage as MessageDescriptor)
: undefined;
graphqlError.extensions.userFriendlyMessage = i18n._(
userFriendlyMessage ?? defaultErrorMessage,
graphqlError.extensions.userFriendlyMessage ?? defaultErrorMessage,
);
return graphqlError;
@@ -24,8 +24,9 @@ export const agentGraphqlApiExceptionHandler = (error: Error) => {
case AgentExceptionCode.AGENT_IS_STANDARD:
case AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
throw new ForbiddenError(error);
case AgentExceptionCode.AGENT_EXECUTION_FAILED:
case AgentExceptionCode.API_KEY_NOT_CONFIGURED:
throw new ForbiddenError(error);
case AgentExceptionCode.AGENT_EXECUTION_FAILED:
case AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
throw error;
default: {
@@ -1,4 +1,4 @@
import { UseGuards } from '@nestjs/common';
import { UseGuards, UseInterceptors } from '@nestjs/common';
import {
Args,
Float,
@@ -51,6 +51,7 @@ import { AgentChatStreamingService } from 'src/engine/metadata-modules/ai/ai-cha
import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service';
import { getCancelChannel } from 'src/engine/metadata-modules/ai/ai-chat/utils/get-cancel-channel.util';
import { SystemPromptBuilderService } from 'src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service';
import { AgentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
@UseGuards(
@@ -58,6 +59,7 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
FeatureFlagGuard,
SettingsPermissionGuard(PermissionFlagType.AI),
)
@UseInterceptors(AgentGraphqlApiExceptionInterceptor)
@MetadataResolver(() => AgentChatThreadDTO)
export class AgentChatResolver {
constructor(