From b1a5c88f67187ac98a9c235cdd859a63ee3ef37f Mon Sep 17 00:00:00 2001 From: Squirbie <246656821+Squirbie@users.noreply.github.com> Date: Wed, 27 May 2026 23:30:20 +0900 Subject: [PATCH] Guard invalid route return values --- src/create-with-winter-spec.ts | 6 +++ tests/errors/do-not-allow-raw-json.test.ts | 50 +++++++++++++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/create-with-winter-spec.ts b/src/create-with-winter-spec.ts index 229a28c..ee41afe 100644 --- a/src/create-with-winter-spec.ts +++ b/src/create-with-winter-spec.ts @@ -127,6 +127,12 @@ function serializeResponse( return async (req, ctx, next) => { const rawResponse = await next(req, ctx) + if (rawResponse == null || typeof rawResponse !== "object") { + throw new Error( + "Use ctx.json({...}) instead of returning an object directly." + ) + } + const statusCode = rawResponse instanceof WinterSpecResponse ? rawResponse.statusCode() diff --git a/tests/errors/do-not-allow-raw-json.test.ts b/tests/errors/do-not-allow-raw-json.test.ts index 8682796..fe24e45 100644 --- a/tests/errors/do-not-allow-raw-json.test.ts +++ b/tests/errors/do-not-allow-raw-json.test.ts @@ -2,6 +2,9 @@ import test from "ava" import { z } from "zod" import { getTestRoute } from "tests/fixtures/get-test-route.js" +const rawResponseGuidance = + "Use ctx.json({...}) instead of returning an object directly" + test("should throw an error when responding with raw JSON", async (t) => { const { axios } = await getTestRoute(t, { globalSpec: { @@ -31,9 +34,46 @@ test("should throw an error when responding with raw JSON", async (t) => { const { data } = await axios.get("/", { validateStatus: () => true, }) - t.true( - data.error.includes( - "Use ctx.json({...}) instead of returning an object directly" - ) - ) + t.true(data.error.includes(rawResponseGuidance)) }) + +const invalidRouteReturns: Array<[string, () => any]> = [ + ["undefined", () => undefined], + ["null", () => null], + ["string", () => "hello"], + ["number", () => 1], + ["boolean", () => false], + ["symbol", () => Symbol("invalid-response")], + ["function", () => () => "hello"], +] + +for (const [returnType, getReturnValue] of invalidRouteReturns) { + test(`should throw ctx.json guidance for ${returnType} route returns`, async (t) => { + const { axios } = await getTestRoute(t, { + globalSpec: { + authMiddleware: {}, + beforeAuthMiddleware: [ + async (req, ctx, next) => { + try { + return await next(req, ctx) + } catch (e: any) { + return Response.json({ error: e.message }, { status: 500 }) + } + }, + ], + }, + routeSpec: { + methods: ["GET"], + jsonBody: z.any(), + jsonResponse: z.any(), + }, + routePath: "/", + routeFn: () => getReturnValue(), + }) + + const { data } = await axios.get("/", { + validateStatus: () => true, + }) + t.true(data.error.includes(rawResponseGuidance)) + }) +}