Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] return 404 instead of 500 for missing agent config name #388

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/routes/agent_routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('test execute agent route', () => {
"headers": Object {},
"payload": Object {
"error": "Internal Server Error",
"message": "Execute agent failed!",
"message": "An internal server error occurred.",
"statusCode": 500,
},
"statusCode": 500,
Expand Down
14 changes: 2 additions & 12 deletions server/routes/agent_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { schema } from '@osd/config-schema';
import { IRouter } from '../../../../src/core/server';
import { AGENT_API } from '../../common/constants/llm';
import { AssistantServiceSetup } from '../services/assistant_service';
import { handleError } from './error_handler';

export function registerAgentRoutes(router: IRouter, assistantService: AssistantServiceSetup) {
router.post(
Expand Down Expand Up @@ -39,18 +40,7 @@ export function registerAgentRoutes(router: IRouter, assistantService: Assistant
);
return res.ok({ body: response });
} catch (e) {
context.assistant_plugin.logger.error('Execute agent failed!', e);
if (e.statusCode >= 400 && e.statusCode <= 499) {
return res.customError({
body: { message: typeof e.body === 'string' ? e.body : JSON.stringify(e.body) },
statusCode: e.statusCode,
});
} else {
return res.customError({
body: 'Execute agent failed!',
statusCode: 500,
});
}
return handleError(e, res, context.assistant_plugin.logger);
}
})
);
Expand Down
83 changes: 83 additions & 0 deletions server/routes/error_handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { handleError } from './error_handler';
import { loggerMock } from '../../../../src/core/server/logging/logger.mock';
import { AgentNotFoundError } from './errors';
import { opensearchDashboardsResponseFactory } from '../../../../src/core/server';

describe('Error handler', () => {
it('should return 404 not found response if error is AgentNotFoundError', () => {
const mockedLogger = loggerMock.create();
const error = handleError(
new AgentNotFoundError('test error'),
opensearchDashboardsResponseFactory,
mockedLogger
);
expect(error.status).toBe(404);
expect(error.options.body).toMatchInlineSnapshot('"Agent not found"');
});

it('should return 4xx with original error body', () => {
const mockedLogger = loggerMock.create();
const error = handleError(
{
statusCode: 429,
body: {
status: 429,
error: {
type: 'OpenSearchStatusException',
reason: 'System Error',
details: 'Request is throttled at model level.',
},
},
},
opensearchDashboardsResponseFactory,
mockedLogger
);
expect(error.status).toBe(429);
expect(error.options.body).toMatchInlineSnapshot(`
Object {
"message": "{\\"status\\":429,\\"error\\":{\\"type\\":\\"OpenSearchStatusException\\",\\"reason\\":\\"System Error\\",\\"details\\":\\"Request is throttled at model level.\\"}}",
}
`);
});

it('shuld return generic 5xx error', () => {
const mockedLogger = loggerMock.create();
const error = handleError(
{
statusCode: 502,
body: {
status: 502,
error: {
type: 'OpenSearchStatusException',
reason: 'System Error',
details: 'Some bad thing happened',
},
},
},
opensearchDashboardsResponseFactory,
mockedLogger
);
expect(error.status).toBe(502);

// No extra info should returned
expect(error.payload).toBe(undefined);
expect(error.options.body).toBe(undefined);
});

it('should return generic internalError for unhandled server-side issues', () => {
const mockedLogger = loggerMock.create();
const error = handleError(
new Error('Arbitrary Error'),
opensearchDashboardsResponseFactory,
mockedLogger
);
expect(error.status).toBe(500);
expect(error.payload).toEqual('Internal Error');
expect(error.options).toMatchInlineSnapshot('Object {}');
});
});
33 changes: 33 additions & 0 deletions server/routes/error_handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { Logger, OpenSearchDashboardsResponseFactory } from '../../../../src/core/server';
import { AgentNotFoundError } from './errors';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const handleError = (e: any, res: OpenSearchDashboardsResponseFactory, logger: Logger) => {
logger.error('Error occurred', e);
// Handle specific type of Errors
if (e instanceof AgentNotFoundError) {
return res.notFound({ body: 'Agent not found' });
}

// handle http response error of calling backend API
if (e.statusCode) {
if (e.statusCode >= 400 && e.statusCode <= 499) {
return res.customError({
body: { message: typeof e.body === 'string' ? e.body : JSON.stringify(e.body) },
statusCode: e.statusCode,
});
} else {
return res.customError({
statusCode: e.statusCode,
});
}
}

// Return an general internalError for unhandled server-side issues
return res.internalError();
};
11 changes: 11 additions & 0 deletions server/routes/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export class AgentNotFoundError extends Error {
constructor(message: string) {
super(message);
this.message = message;
}
}
77 changes: 34 additions & 43 deletions server/routes/get_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,55 @@

import { OpenSearchClient } from '../../../../src/core/server';
import { ML_COMMONS_BASE_API } from '../utils/constants';
import { AgentNotFoundError } from './errors';

/**
*
*/
export const getAgentIdByConfigName = async (
configName: string,
client: OpenSearchClient['transport']
): Promise<string> => {
try {
const path = `${ML_COMMONS_BASE_API}/config/${configName}`;
const response = await client.request({
method: 'GET',
path,
});
const path = `${ML_COMMONS_BASE_API}/config/${configName}`;
const response = await client.request({
method: 'GET',
path,
});

if (
!response ||
!(response.body.ml_configuration?.agent_id || response.body.configuration?.agent_id)
) {
throw new Error(`cannot get agent ${configName} by calling the api: ${path}`);
}
return response.body.ml_configuration?.agent_id || response.body.configuration.agent_id;
} catch (error) {
const errorMessage = JSON.stringify(error.meta?.body) || error;
throw new Error(`get agent ${configName} failed, reason: ${errorMessage}`);
if (
!response ||
!(response.body.ml_configuration?.agent_id || response.body.configuration?.agent_id)
) {
throw new AgentNotFoundError(
`cannot get agent by config name ${configName} by calling the api: ${path}`
);
}
return response.body.ml_configuration?.agent_id || response.body.configuration.agent_id;
};

export const searchAgent = async (
{ name }: { name: string },
client: OpenSearchClient['transport']
) => {
try {
const requestParams = {
query: {
term: {
'name.keyword': name,
},
},
_source: ['_id'],
sort: {
created_time: 'desc',
const requestParams = {
query: {
term: {
'name.keyword': name,
},
size: 1,
};
},
_source: ['_id'],
sort: {
created_time: 'desc',
},
size: 1,
};

const response = await client.request({
method: 'GET',
path: `${ML_COMMONS_BASE_API}/agents/_search`,
body: requestParams,
});
const path = `${ML_COMMONS_BASE_API}/agents/_search`;
const response = await client.request({
method: 'GET',
path,
body: requestParams,
});

if (!response || response.body.hits.total.value === 0) {
return undefined;
}
return response.body.hits.hits[0]._id;
} catch (error) {
const errorMessage = JSON.stringify(error.meta?.body) || error;
throw new Error(`search ${name} agent failed, reason: ` + errorMessage);
if (!response || response.body.hits.total.value === 0) {
throw new AgentNotFoundError(`cannot find agent by name ${name} by calling the api: ${path}`);
}
return response.body.hits.hits[0]._id as string;
};
19 changes: 13 additions & 6 deletions server/routes/summary_routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SUMMARY_ASSISTANT_API } from '../../common/constants/llm';
import { registerData2SummaryRoutes, registerSummaryAssistantRoutes } from './summary_routes';
import { AssistantClient } from '../services/assistant_client';
import { RequestHandlerContext } from '../../../../src/core/server';
import * as AgentHelpers from './get_agent';
const mockedLogger = loggerMock.create();

export const createMockedAssistantClient = (
Expand Down Expand Up @@ -147,7 +148,7 @@ describe('test summary route', () => {
"headers": Object {},
"payload": Object {
"error": "Internal Server Error",
"message": "Execute agent failed!",
"message": "An internal server error occurred.",
"statusCode": 500,
},
"statusCode": 500,
Expand All @@ -167,9 +168,10 @@ describe('test summary route', () => {
},
},
});
const spy = jest.spyOn(AgentHelpers, 'getAgentIdByConfigName').mockResolvedValue('test_agent');
const result = (await insightRequest({
summaryType: 'alerts',
insightType: 'test',
insightType: 'os_insight',
summary: 'summary',
question: 'Please summarize this alert, do not use any tool.',
context: 'context',
Expand All @@ -185,16 +187,18 @@ describe('test summary route', () => {
"statusCode": 429,
}
`);
spy.mockRestore();
});

it('return 4xx when executeAgent throws 4xx error in string format for insight API', async () => {
mockedAssistantClient.executeAgent = jest.fn().mockRejectedValue({
statusCode: 429,
body: 'Request is throttled at model level',
});
const spy = jest.spyOn(AgentHelpers, 'getAgentIdByConfigName').mockResolvedValue('test_agent');
const result = (await insightRequest({
summaryType: 'alerts',
insightType: 'test',
insightType: 'os_insight',
summary: 'summary',
question: 'Please summarize this alert, do not use any tool.',
context: 'context',
Expand All @@ -210,16 +214,18 @@ describe('test summary route', () => {
"statusCode": 429,
}
`);
spy.mockRestore();
});

it('return 5xx when executeAgent throws 5xx for insight API', async () => {
mockedAssistantClient.executeAgent = jest.fn().mockRejectedValue({
statusCode: 500,
body: 'Server error',
});
const spy = jest.spyOn(AgentHelpers, 'getAgentIdByConfigName').mockResolvedValue('test_agent');
const result = (await insightRequest({
summaryType: 'alerts',
insightType: 'test',
insightType: 'os_insight',
summary: 'summary',
question: 'Please summarize this alert, do not use any tool.',
context: 'context',
Expand All @@ -229,12 +235,13 @@ describe('test summary route', () => {
"headers": Object {},
"payload": Object {
"error": "Internal Server Error",
"message": "Execute agent failed!",
"message": "An internal server error occurred.",
"statusCode": 500,
},
"statusCode": 500,
}
`);
spy.mockRestore();
});

it('return 4xx when execute agent throws 4xx error for data2Summary API', async () => {
Expand Down Expand Up @@ -312,7 +319,7 @@ describe('test summary route', () => {
"headers": Object {},
"payload": Object {
"error": "Internal Server Error",
"message": "Execute agent failed!",
"message": "An internal server error occurred.",
"statusCode": 500,
},
"statusCode": 500,
Expand Down
Loading
Loading