From 81d0d3b5c5c6f35381bd278fd6c36e504efbff1c Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Mon, 27 Apr 2026 10:40:12 +0200 Subject: [PATCH 1/5] fix(api-service): surface provider delivery errors in agents flow fixes NV-7410 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap all three provider delivery boundaries (thread.post, adapter.editMessage, handler.send) in chat-sdk.service.ts with .catch(toDeliveryError), converting any unhandled provider error into a BadGatewayException (502) that preserves the original error message instead of swallowing it as a generic 500. Add AgentDeliveryError to @novu/framework so developers can instanceof-check delivery failures in their agent handlers and react to them explicitly. Old framework versions automatically get better error messages from the 502 body without any code changes — the new typed error is additive. Made-with: Cursor --- .../app/agents/services/chat-sdk.service.ts | 31 ++++++++++++------ packages/framework/src/index.ts | 1 + .../src/resources/agent/agent.context.ts | 13 ++++++++ .../src/resources/agent/agent.errors.ts | 32 +++++++++++++++++++ .../framework/src/resources/agent/index.ts | 1 + 5 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 packages/framework/src/resources/agent/agent.errors.ts diff --git a/apps/api/src/app/agents/services/chat-sdk.service.ts b/apps/api/src/app/agents/services/chat-sdk.service.ts index 884d3eaeab0..d81e0948481 100644 --- a/apps/api/src/app/agents/services/chat-sdk.service.ts +++ b/apps/api/src/app/agents/services/chat-sdk.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, OnModuleDestroy } from '@nestjs/common'; +import { BadGatewayException, BadRequestException, Injectable, OnModuleDestroy } from '@nestjs/common'; import { CacheService, decryptCredentials, MailFactory, PinoLogger } from '@novu/application-generic'; import { IntegrationRepository } from '@novu/dal'; import type { SentMessageInfo } from '@novu/framework'; @@ -14,6 +14,13 @@ import { sendWebResponse, toWebRequest } from '../utils/express-to-web-request'; import { AgentConfigResolver, ResolvedAgentConfig } from './agent-config-resolver.service'; import { AgentInboundHandler } from './agent-inbound-handler.service'; +function toDeliveryError(err: unknown): never { + throw new BadGatewayException({ + error: 'delivery_failed', + message: err instanceof Error ? err.message : String(err), + }); +} + /** Ensure a Message-ID value is wrapped in RFC 5322 angle brackets. */ function wrapMsgId(id: string): string { const trimmed = id.trim(); @@ -134,15 +141,17 @@ export class ChatSdkService implements OnModuleDestroy { const adapter = chat.getAdapter(platform); const thread = ThreadImpl.fromJSON(serializedThread, adapter); - let sent: { id: string; threadId: string }; + let postPromise: Promise<{ id: string; threadId: string }>; if (content.card) { - sent = await thread.post(content.card); + postPromise = thread.post(content.card); } else if (content.markdown !== undefined) { - sent = await thread.post({ markdown: content.markdown, files: content.files }); + postPromise = thread.post({ markdown: content.markdown, files: content.files }); } else { - sent = await thread.post(content.text ?? ''); + postPromise = thread.post(content.text ?? ''); } + const sent = await postPromise.catch(toDeliveryError); + return { messageId: sent.id, platformThreadId: sent.threadId }; } @@ -163,22 +172,24 @@ export class ChatSdkService implements OnModuleDestroy { throw new BadRequestException(`Platform ${platform} does not support editing messages`); } - let edited: { id: string; threadId: string }; + let editPromise: Promise<{ id: string; threadId: string }>; if (content.card) { - edited = await adapter.editMessage( + editPromise = adapter.editMessage( platformThreadId, platformMessageId, content.card as unknown as AdapterPostableMessage ); } else if (content.markdown !== undefined) { - edited = await adapter.editMessage(platformThreadId, platformMessageId, { + editPromise = adapter.editMessage(platformThreadId, platformMessageId, { markdown: content.markdown, files: content.files, } as unknown as AdapterPostableMessage); } else { - edited = await adapter.editMessage(platformThreadId, platformMessageId, content.text ?? ''); + editPromise = adapter.editMessage(platformThreadId, platformMessageId, content.text ?? ''); } + const edited = await editPromise.catch(toDeliveryError); + return { messageId: edited.id, platformThreadId: edited.threadId }; } @@ -419,7 +430,7 @@ export class ChatSdkService implements OnModuleDestroy { }, }; - const result = await handler.send(mailOptions); + const result = await handler.send(mailOptions).catch(toDeliveryError); return { messageId: result?.id || params.messageId || '' }; }; diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index d584f80910e..fc76897d05b 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -30,6 +30,7 @@ export type { } from './resources'; export { Actions, + AgentDeliveryError, AgentEventEnum, agent, Button, diff --git a/packages/framework/src/resources/agent/agent.context.ts b/packages/framework/src/resources/agent/agent.context.ts index d40d25533e1..deb261c78c5 100644 --- a/packages/framework/src/resources/agent/agent.context.ts +++ b/packages/framework/src/resources/agent/agent.context.ts @@ -1,4 +1,5 @@ import { isJSX, toCardElement } from 'chat/jsx-runtime'; +import { AgentDeliveryError } from './agent.errors'; import type { AgentAction, AgentBridgeRequest, @@ -216,6 +217,18 @@ export class AgentContextImpl implements AgentContext { if (!response.ok) { const text = await response.text().catch(() => ''); + + if (response.status === 502) { + let message = text; + try { + const parsed = JSON.parse(text) as { message?: string }; + if (parsed.message) message = parsed.message; + } catch { + // use raw text if JSON parsing fails + } + throw new AgentDeliveryError(502, message); + } + throw new Error(`Agent reply failed (${response.status}): ${text}`); } diff --git a/packages/framework/src/resources/agent/agent.errors.ts b/packages/framework/src/resources/agent/agent.errors.ts new file mode 100644 index 00000000000..349955594b1 --- /dev/null +++ b/packages/framework/src/resources/agent/agent.errors.ts @@ -0,0 +1,32 @@ +/** + * Thrown by `ctx.reply()` and `handle.edit()` when the upstream message delivery + * fails — e.g. the configured email provider returns 401, Slack rejects the token, + * or Teams rejects the request. + * + * The `message` property contains the original provider error text + * + * @example + * ```ts + * import { AgentDeliveryError } from '@novu/framework'; + * + * try { + * await ctx.reply('Hello!'); + * } catch (err) { + * if (err instanceof AgentDeliveryError) { + * // Delivery failed (misconfigured provider, rate limit, etc.) + * console.error('Delivery failed:', err.message); + * return; + * } + * throw err; + * } + * ``` + */ +export class AgentDeliveryError extends Error { + readonly statusCode: number; + + constructor(statusCode: number, message: string) { + super(message); + this.name = 'AgentDeliveryError'; + this.statusCode = statusCode; + } +} diff --git a/packages/framework/src/resources/agent/index.ts b/packages/framework/src/resources/agent/index.ts index 7fd2a81445f..3b6edfbc3b4 100644 --- a/packages/framework/src/resources/agent/index.ts +++ b/packages/framework/src/resources/agent/index.ts @@ -12,6 +12,7 @@ export type { } from 'chat'; export { Actions, Button, Card, CardLink, CardText, Divider, Select, SelectOption, TextInput } from 'chat'; export { AgentContextImpl } from './agent.context'; +export { AgentDeliveryError } from './agent.errors'; export { agent } from './agent.resource'; export type { Agent, From 97198b7dde074ac87ab2f7a013d6cffe00bebb4a Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Mon, 27 Apr 2026 11:04:23 +0200 Subject: [PATCH 2/5] fix(api-service): surface provider errors in send-agent-test-email endpoint fixes NV-7410 Same unguarded handler.send() pattern as the delivery path. Wrap it with .catch() so provider errors (invalid API key, 401, etc.) are returned as BadGatewayException (502) with the original message, making the dashboard toast actionable instead of showing a generic internal server error. Made-with: Cursor --- .../send-agent-test-email.usecase.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts index 49b6878928c..aa67372b7b0 100644 --- a/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts +++ b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadGatewayException, BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { decryptCredentials, InstrumentUsecase, MailFactory } from '@novu/application-generic'; import { AgentIntegrationRepository, AgentRepository, IntegrationRepository } from '@novu/dal'; import { ChannelTypeEnum, EmailProviderIdEnum, IEmailOptions } from '@novu/shared'; @@ -85,7 +85,12 @@ export class SendAgentTestEmail { senderName: (senderIntegration.credentials?.senderName as string) || 'Novu', }; - await handler.send(mailOptions); + await handler.send(mailOptions).catch((err) => { + throw new BadGatewayException({ + error: 'delivery_failed', + message: err instanceof Error ? err.message : String(err), + }); + }); return { success: true }; } From 4a09c63fedb2277c4248b446cbb0e923588b8971 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Mon, 27 Apr 2026 11:23:10 +0200 Subject: [PATCH 3/5] fix(api-service): improve provider error message extraction in agents delivery Extract nested error detail from common REST API error shapes (errors[0].message or body.message) so the surfaced message is actionable rather than just the HTTP status text e.g. "Unauthorized: The provided authorization grant is invalid" instead of just "Unauthorized". Made-with: Cursor --- apps/api/src/app/agents/services/chat-sdk.service.ts | 5 ++++- .../send-agent-test-email/send-agent-test-email.usecase.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/agents/services/chat-sdk.service.ts b/apps/api/src/app/agents/services/chat-sdk.service.ts index d81e0948481..aa5c976d880 100644 --- a/apps/api/src/app/agents/services/chat-sdk.service.ts +++ b/apps/api/src/app/agents/services/chat-sdk.service.ts @@ -15,9 +15,12 @@ import { AgentConfigResolver, ResolvedAgentConfig } from './agent-config-resolve import { AgentInboundHandler } from './agent-inbound-handler.service'; function toDeliveryError(err: unknown): never { + const base = err instanceof Error ? err.message : String(err); + const body = (err as any)?.response?.body; + const detail = Array.isArray(body?.errors) ? body.errors[0]?.message : body?.message; throw new BadGatewayException({ error: 'delivery_failed', - message: err instanceof Error ? err.message : String(err), + message: detail ? `${base}: ${detail}` : base, }); } diff --git a/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts index aa67372b7b0..06e02d9c80a 100644 --- a/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts +++ b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts @@ -86,9 +86,12 @@ export class SendAgentTestEmail { }; await handler.send(mailOptions).catch((err) => { + const base = err instanceof Error ? err.message : String(err); + const body = (err as any)?.response?.body; + const detail = Array.isArray(body?.errors) ? body.errors[0]?.message : body?.message; throw new BadGatewayException({ error: 'delivery_failed', - message: err instanceof Error ? err.message : String(err), + message: detail ? `${base}: ${detail}` : base, }); }); From e2e5ede3dcbb26935f52a17738837a7beabc2eb4 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Mon, 27 Apr 2026 11:37:44 +0200 Subject: [PATCH 4/5] chore(framework): bump @novu/framework to 2.10.1-alpha.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentDeliveryError is a new public export — version bump required. Made-with: Cursor --- packages/framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework/package.json b/packages/framework/package.json index 589992a37b6..98ab79c37c6 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -1,6 +1,6 @@ { "name": "@novu/framework", - "version": "2.10.1-alpha.2", + "version": "2.10.1-alpha.3", "description": "The Code-First Notifications Workflow SDK.", "main": "./dist/cjs/index.cjs", "types": "./dist/cjs/index.d.cts", From ea08eedd63308f82b9ea12309fd9f87d5278efe3 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Mon, 27 Apr 2026 11:38:10 +0200 Subject: [PATCH 5/5] Revert "chore(framework): bump @novu/framework to 2.10.1-alpha.3" This reverts commit e2e5ede3dcbb26935f52a17738837a7beabc2eb4. --- packages/framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework/package.json b/packages/framework/package.json index 98ab79c37c6..589992a37b6 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -1,6 +1,6 @@ { "name": "@novu/framework", - "version": "2.10.1-alpha.3", + "version": "2.10.1-alpha.2", "description": "The Code-First Notifications Workflow SDK.", "main": "./dist/cjs/index.cjs", "types": "./dist/cjs/index.d.cts",