diff --git a/src/middleware/http-exceptions.ts b/src/middleware/http-exceptions.ts index 9e0f658..fa68d00 100644 --- a/src/middleware/http-exceptions.ts +++ b/src/middleware/http-exceptions.ts @@ -70,3 +70,9 @@ export class ResponseValidationError extends WinterSpecMiddlewareError { super(formatZodError(error), 500) } } + +export class ResponseObjectNotAllowedError extends WinterSpecMiddlewareError { + constructor() { + super("Use ctx.json({...}) instead of returning an object directly.", 500) + } +} diff --git a/src/middleware/with-response-object-check.ts b/src/middleware/with-response-object-check.ts index 9f8cae0..25b3a37 100644 --- a/src/middleware/with-response-object-check.ts +++ b/src/middleware/with-response-object-check.ts @@ -1,4 +1,4 @@ -import { ResponseValidationError } from "./http-exceptions.js" +import { ResponseObjectNotAllowedError } from "./http-exceptions.js" import { Middleware } from "./types.js" import { RouteSpec } from "src/types/route-spec.js" @@ -9,9 +9,7 @@ export const withResponseObjectCheck: Middleware< const rawResponse = await next(req, ctx) if (typeof rawResponse === "object" && !(rawResponse instanceof Response)) { - throw new Error( - "Use ctx.json({...}) instead of returning an object directly." - ) + throw new ResponseObjectNotAllowedError() } return rawResponse diff --git a/tests/middleware/with-response-object-check.test.ts b/tests/middleware/with-response-object-check.test.ts new file mode 100644 index 0000000..bba30d0 --- /dev/null +++ b/tests/middleware/with-response-object-check.test.ts @@ -0,0 +1,39 @@ +import test from "ava" +import { z } from "zod" +import { getTestRoute } from "../fixtures/get-test-route.js" +import { createWithDefaultExceptionHandling } from "../../src/middleware/with-default-exception-handling.js" + +test("returning a plain object with jsonResponse validation under default exception handling", async (t) => { + const { axios } = await getTestRoute(t, { + globalSpec: { + authMiddleware: {}, + beforeAuthMiddleware: [createWithDefaultExceptionHandling()], + }, + routeSpec: { + auth: "none", + methods: ["GET"], + jsonResponse: z.object({ + ok: z.boolean(), + message: z.string(), + }), + }, + routeFn: (_, ctx) => { + // Accidental plain object return + return { ok: true, message: "hello" } as any + }, + routePath: "/test-plain-object", + }) + + const response = await axios.get("/test-plain-object", { + validateStatus: () => true, + }) + + console.log("Response status:", response.status) + console.log("Response data:", response.data) + + t.is(response.status, 500) + t.is( + response.data.message, + "Use ctx.json({...}) instead of returning an object directly." + ) +})