Skip to content
Merged
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
19 changes: 19 additions & 0 deletions src/routes/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { fastify, fastifyAfter, fastifyBefore, opensearch, prisma } from '../test/helpers/fastify.js';
import { AllPublicAccessTransformer } from '../transformers/default.js';
import type { StandardErrorResponse } from '../utils/errors.js';
import searchRoute from './search.js';

describe('Search Route', () => {
Expand Down Expand Up @@ -379,6 +380,24 @@ describe('Search Route', () => {
);
});

it('should return 422 for malformed opensearch query', async () => {
const opensearchError = Object.assign(new Error('parsing_exception'), { statusCode: 400 });
opensearch.search.mockRejectedValue(opensearchError);

const response = await fastify.inject({
method: 'POST',
url: '/search',
payload: {
query: 'test',
searchType: 'advanced',
},
});

expect(response.statusCode).toBe(422);
const body = JSON.parse(response.body) as StandardErrorResponse;
expect(body.error.code).toBe('INVALID_REQUEST');
});

it('should handle opensearch errors', async () => {
opensearch.search.mockRejectedValue(new Error('OpenSearch connection failed'));

Expand Down
10 changes: 9 additions & 1 deletion src/routes/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ZodTypeProvider } from 'fastify-type-provider-zod';
import { z } from 'zod/v4';
import { baseEntityTransformer, resolveEntityReferences } from '../transformers/default.js';
import type { AccessTransformer, EntityTransformer } from '../types/transformers.js';
import { createInternalError } from '../utils/errors.js';
import { createInternalError, createInvalidRequestError } from '../utils/errors.js';
import { OpensearchQueryBuilder, type QueryBuilderOptions } from '../utils/queryBuilder.js';

const boundingBoxSchema = z.object({
Expand Down Expand Up @@ -183,6 +183,14 @@ const search: FastifyPluginAsync<SearchRouteOptions> = async (fastify, opts) =>
return result;
} catch (error) {
const err = error as Error;

// OpenSearch returns 400 for malformed queries (e.g. invalid query_string syntax)
if ('statusCode' in err && (err as { statusCode: number }).statusCode === 400) {
fastify.log.warn(`Invalid search query: ${err.message}`);

return reply.code(422).send(createInvalidRequestError(err.message));
}

fastify.log.error(`Search error: ${err.message}`);

return reply.code(500).send(createInternalError('Search failed'));
Expand Down
3 changes: 3 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const createValidationError = (message: string, violations: ValidationVio
export const createNotFoundError = (message: string, entityId?: string): StandardErrorResponse =>
createErrorResponse(ERROR_CODES.NOT_FOUND, message, entityId ? { entityId } : undefined);

export const createInvalidRequestError = (message: string): StandardErrorResponse =>
createErrorResponse(ERROR_CODES.INVALID_REQUEST, message);

export const createInternalError = (message = 'Internal server error'): StandardErrorResponse => {
return createErrorResponse(ERROR_CODES.INTERNAL_ERROR, message);
};
Loading