🛑 refactor: Demote User Abort Logs (#13904)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run

* fix: Demote user abort logging

* fix: Handle abort causes

* fix: Demote user-aborted agent completion to debug log

The error users still saw originated in AgentClient's completion catch,
which logged every caught error (including user aborts) at error level
before checking the abort signal. Branch on abortController.signal.aborted
so user-initiated aborts log at debug while real failures stay error-classified.

Also give the handleAbortError it.each cases distinct titles.
This commit is contained in:
Danny Avila 2026-06-23 09:55:21 -04:00 • committed by GitHub
parent cb2bafb457
commit bc6b032421
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 120 additions and 8 deletions

View file

@ -1450,11 +1450,12 @@ class AgentClient extends BaseClient {
});
}
} catch (err) {
logger.error(
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted',
err,
);
if (!abortController.signal.aborted) {
if (abortController.signal.aborted) {
logger.debug(
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted by user',
{ conversationId: this.conversationId, name: err?.name, code: err?.code },
);
} else {
logger.error(
'[api/server/controllers/agents/client.js #sendCompletion] Unhandled error type',
err,

View file

@ -15,6 +15,36 @@ const { sendError } = require('~/server/middleware/error');
const { abortRun } = require('./abortRun');
const db = require('~/models');
/**
* @param {Error | unknown} error
* @returns {boolean}
*/
const isAbortError = (error) => {
const visited = new Set();
let current = error;
while (current && typeof current === 'object' && !visited.has(current)) {
visited.add(current);
const errorName = current.name;
const errorCode = current.code;
const errorMessage = typeof current.message === 'string' ? current.message : '';
if (
errorName === 'AbortError' ||
errorCode === 'ABORT_ERR' ||
errorMessage.includes('AbortError') ||
/(?:operation|request|stream) was aborted/i.test(errorMessage)
) {
return true;
}
current = current.cause;
}
return false;
};
/**
* Spend tokens for all models from collected usage.
* This handles both sequential and parallel agent execution.
@ -200,18 +230,26 @@ const handleAbort = function () {
* @returns {Promise<void>}
*/
const handleAbortError = async (res, req, error, data) => {
const { sender, conversationId, messageId, parentMessageId, userMessageId, partialText } = data;
if (error?.message?.includes('base64')) {
logger.error('[handleAbortError] Error in base64 encoding', {
...error,
stack: smartTruncateText(error?.stack, 1000),
message: truncateText(error.message, 350),
});
} else if (isAbortError(error)) {
logger.debug('[handleAbortError] AI response aborted by user', {
conversationId,
code: error?.code,
name: error?.name,
message: truncateText(error?.message ?? 'AbortError', 350),
});
} else {
logger.error('[handleAbortError] AI response error; aborting request:', error);
}
const { sender, conversationId, messageId, parentMessageId, userMessageId, partialText } = data;
if (error.stack && error.stack.includes('google')) {
if (error?.stack && error.stack.includes('google')) {
logger.warn(
`AI Response error for conversation ${conversationId} likely caused by Google censor/filter`,
);

View file

@ -73,7 +73,18 @@ jest.mock('./abortRun', () => ({
abortRun: jest.fn(),
}));
const { spendCollectedUsage } = require('./abortMiddleware');
const { logger } = require('@librechat/data-schemas');
const { sendError } = require('~/server/middleware/error');
const { handleAbortError, spendCollectedUsage } = require('./abortMiddleware');
const buildAbortRequest = () => ({
body: {
model: 'gpt-4',
},
user: {
id: 'user-123',
},
});
describe('abortMiddleware - spendCollectedUsage', () => {
beforeEach(() => {
@ -237,3 +248,65 @@ describe('abortMiddleware - spendCollectedUsage', () => {
});
});
});
describe('abortMiddleware - handleAbortError', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it.each([
[
'native DOMException AbortError',
new DOMException('The operation was aborted', 'AbortError'),
'AbortError',
],
[
'wrapped AbortError message',
new Error('SSE stream disconnected: AbortError: The operation was aborted'),
'Error',
],
[
'cause-nested AbortError',
new Error('Request failed', {
cause: new DOMException('The operation was aborted', 'AbortError'),
}),
'Error',
],
])('logs a %s as a debug event instead of an error', async (_label, error, name) => {
await handleAbortError({}, buildAbortRequest(), error, {
sender: 'AI',
conversationId: 'convo-123',
messageId: 'message-123',
parentMessageId: 'parent-123',
userMessageId: 'user-message-123',
});
expect(logger.error).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledWith('[handleAbortError] AI response aborted by user', {
conversationId: 'convo-123',
code: error.code,
name,
message: error.message,
});
expect(sendError).toHaveBeenCalledTimes(1);
});
it('keeps unexpected generation errors classified as errors', async () => {
const error = new Error('Provider failed');
await handleAbortError({}, buildAbortRequest(), error, {
sender: 'AI',
conversationId: 'convo-123',
messageId: 'message-123',
parentMessageId: 'parent-123',
userMessageId: 'user-message-123',
});
expect(logger.error).toHaveBeenCalledWith(
'[handleAbortError] AI response error; aborting request:',
error,
);
expect(logger.debug).not.toHaveBeenCalled();
expect(sendError).toHaveBeenCalledTimes(1);
});
});