diff --git a/.changeset/cuddly-buses-obey.md b/.changeset/cuddly-buses-obey.md new file mode 100644 index 000000000..01fddf2d6 --- /dev/null +++ b/.changeset/cuddly-buses-obey.md @@ -0,0 +1,5 @@ +--- +"@logto/core": patch +--- + +Management API will not return 500 in production for status codes that are not listed in the OpenAPI spec diff --git a/packages/core/src/middleware/koa-guard.test.ts b/packages/core/src/middleware/koa-guard.test.ts index 825e8b7d7..9d30bd42a 100644 --- a/packages/core/src/middleware/koa-guard.test.ts +++ b/packages/core/src/middleware/koa-guard.test.ts @@ -1,6 +1,7 @@ import { createMockUtils } from '@logto/shared/esm'; import { z } from 'zod'; +import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import ServerError from '#src/errors/ServerError/index.js'; import { emptyMiddleware, createContextWithRouteParameters } from '#src/utils/test-utils.js'; @@ -135,6 +136,26 @@ describe('koaGuardMiddleware', () => { await expect(koaGuard({ status: [200, 204] })(ctx, next)).rejects.toThrow(ServerError); }); + it('should not throw when status is invalid in production', async () => { + const ctx = { + ...baseCtx, + params: {}, + body: {}, + guard: {}, + response: { status: 301 }, + }; + const { isProduction } = EnvSet.values; + + // eslint-disable-next-line @silverhand/fp/no-mutating-assign + Object.assign(EnvSet.values, { isProduction: true }); + // @ts-expect-error + await expect(koaGuard({ status: 200 })(ctx, next)).resolves.toBeUndefined(); + // @ts-expect-error + await expect(koaGuard({ status: [200, 204] })(ctx, next)).resolves.toBeUndefined(); + // eslint-disable-next-line @silverhand/fp/no-mutating-assign + Object.assign(EnvSet.values, { isProduction }); + }); + it('should throw when inner middleware throws invalid status', async () => { const ctx = { ...baseCtx, diff --git a/packages/core/src/middleware/koa-guard.ts b/packages/core/src/middleware/koa-guard.ts index 8506aa67f..79d5bbcdc 100644 --- a/packages/core/src/middleware/koa-guard.ts +++ b/packages/core/src/middleware/koa-guard.ts @@ -1,3 +1,4 @@ +import { appInsights } from '@logto/app-insights/node'; import type { Optional } from '@silverhand/essentials'; import { has } from '@silverhand/essentials'; import type { MiddlewareType } from 'koa'; @@ -8,7 +9,6 @@ import type { ZodType, ZodTypeDef } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import { ResponseBodyError, StatusCodeError } from '#src/errors/ServerError/index.js'; -import assertThat from '#src/utils/assert-that.js'; import { consoleLog } from '#src/utils/console.js'; /** Configure what and how to guard. */ @@ -174,6 +174,9 @@ export default function koaGuard< * Assert the status code matches the value(s) in the config. If the config does not * specify a status code, it will not assert anything. * + * In production, it will log a warning if the status code does not match the value(s) in the + * config for better user experience. + * * @param value The status code to assert. * @throws {StatusCodeError} If the status code does not match the value(s) in the config. */ @@ -182,10 +185,17 @@ export default function koaGuard< return; } - assertThat( - Array.isArray(status) ? status.includes(value) : status === value, - new StatusCodeError(status, value) - ); + if (Array.isArray(status) ? status.includes(value) : status === value) { + return; + } + + if (EnvSet.values.isProduction) { + consoleLog.warn('Unexpected status code:', value, 'expected:', status); + void appInsights.trackException(new StatusCodeError(status, value)); + return; + } + + throw new StatusCodeError(status, value); }; try { @@ -215,10 +225,7 @@ export default function koaGuard< // the properties that are not defined in the schema. ctx.body = result.data; } else { - if (!EnvSet.values.isProduction) { - consoleLog.error('Invalid response:', result.error); - } - + consoleLog.error('Invalid response:', result.error); throw new ResponseBodyError(result.error); } }