From 5c702852965e6f1f0f88513b4b8c21f144bcb3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Levilain?= Date: Fri, 13 Jun 2025 14:58:05 +0200 Subject: [PATCH] feat: properly return an Unprocessable Entity error in case of expected handler rejection --- src/api/models/controllers.test.ts | 22 ++++++++++++++++++++++ src/models/handler.test.ts | 18 +++++++++++++++++- src/models/handler.ts | 17 +++++++++++++++++ src/setup.ts | 16 +++++++++++++--- test/fixtures/users.ts | 5 +++++ 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/api/models/controllers.test.ts b/src/api/models/controllers.test.ts index 0f3ebdd..0228de0 100644 --- a/src/api/models/controllers.test.ts +++ b/src/api/models/controllers.test.ts @@ -967,6 +967,28 @@ describe('controllers/models', () => { expect(error.status).toEqual(422); }); + it('returns a 422 Unprocessable Entity if the event handler rejects the event', async () => { + const controller = apply({ ...services, models }); + + const user = models.factory('users'); + + await user.create({ firstname: 'John' }); + + req.params.model = 'users'; + req.params.correlation_id = user.state.user_id.toString(); + req.params.event_type = 'always_unprocessable'; + req.params.event_version = '0_0_0'; + req.body = {}; + + await controller(req, res, next); + + expect(error).not.toBe(null); + expect(error.message).toEqual( + 'Reducer failed to process event: Always Unprocessable', + ); + expect(error.status).toEqual(422); + }); + it('returns a 405 `Entity is readonly` in case of an apply on a readonly entity', async () => { const controller = apply({ ...services, models }); diff --git a/src/models/handler.test.ts b/src/models/handler.test.ts index b326c74..382b0a6 100644 --- a/src/models/handler.test.ts +++ b/src/models/handler.test.ts @@ -1,4 +1,4 @@ -import { handle } from './handler'; +import { handle, UnprocessableReducerError } from './handler'; describe('models/handler', () => { const DEFAULT_HANDLER = ` @@ -23,4 +23,20 @@ describe('models/handler', () => { expect(error.message.startsWith('Cannot read properties')).toEqual(true); }); + + it('throws an UnprocessableReducerError when a UnprocessableError is thrown by the code', async () => { + let error; + try { + await handle(`throw new UnprocessableError('Forbidden update');`, null, { + count: 1, + }); + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(UnprocessableReducerError); + expect(error.message).toEqual( + 'Reducer failed to process event: Forbidden update', + ); + }); }); diff --git a/src/models/handler.ts b/src/models/handler.ts index f0e1651..1ac2f1e 100644 --- a/src/models/handler.ts +++ b/src/models/handler.ts @@ -1,10 +1,18 @@ import type FullyHomomorphicEncryptionClient from '../services/fhe'; +import statusCodes from 'http-status-codes'; import path from 'node:path'; import { Worker } from 'worker_threads'; import { ModelConfig } from '../typings'; +export class UnprocessableReducerError extends Error { + readonly status = statusCodes.UNPROCESSABLE_ENTITY; + constructor(message: string) { + super(`Reducer failed to process event: ${message}`); + } +} + export async function handle( handler: string | Function, state: any, @@ -103,6 +111,10 @@ export async function thread( const workerScript = ` const { parentPort, workerData } = require('worker_threads'); +class UnprocessableError extends Error { + type = 'UnprocessableError'; +} + ${script} async function main() { @@ -125,6 +137,11 @@ main().catch((err) => { }); thread.on('error', (err) => { + if ('type' in err && err.type === 'UnprocessableError') { + reject(new UnprocessableReducerError(err.message)); + return; + } + reject(err); }); thread.on('exit', () => { diff --git a/src/setup.ts b/src/setup.ts index f7dbca6..45f6bf3 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,7 +1,17 @@ -process.env.MONGOMS_DOWNLOAD_URL = - process.env.MONGOMS_DOWNLOAD_URL || - 'https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian12-8.0.3.tgz'; +import * as os from 'node:os'; + +const [SYSTEM_PLATFORM, SYSTEM_MACHINE] = [os.platform(), os.machine()]; +const FALLBACK_MONGOMS_DOWNLOAD_URL = + SYSTEM_PLATFORM === 'linux' + ? `https://fastdl.mongodb.org/linux/mongodb-linux-${SYSTEM_MACHINE}-debian12-8.0.3.tgz` + : SYSTEM_PLATFORM === 'darwin' + ? `https://fastdl.mongodb.org/osx/mongodb-macos-${SYSTEM_MACHINE}-8.0.3.tgz` + : (function () { + throw new Error('Unsupported system architecture'); + })(); +process.env.MONGOMS_DOWNLOAD_URL = + process.env.MONGOMS_DOWNLOAD_URL || FALLBACK_MONGOMS_DOWNLOAD_URL; process.env.OTEL_PROMETHEUS_EXPORTER_PREVENT_SERVER_START = 'true'; import type { DatastoreConfig, ModelConfig, Services } from './typings'; diff --git a/test/fixtures/users.ts b/test/fixtures/users.ts index f65f27c..884c8ab 100644 --- a/test/fixtures/users.ts +++ b/test/fixtures/users.ts @@ -145,6 +145,11 @@ const modelConfig: ModelConfig = { }, }, }, + ALWAYS_UNPROCESSABLE: { + '0_0_0': { + handler: `throw new UnprocessableError('Always Unprocessable');`, + } + } }, }, processings: [ -- GitLab