Repository: RobinTail/express-zod-api
Pull request #735 (2022-12-10 22:45:58)

Output validation HTTP code
Output validation error should cause HTTP 500 (instead of 400).

Also changing the error message slightly, from
```
output: Invalid format; anything: Number must be greater than 0
```
to
```
output/anything: Number must be greater than 0
```


Patch:
diff --git a/src/endpoint.ts b/src/endpoint.ts
index 070a1564..0023fddf 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -3,11 +3,16 @@ import { Logger } from "winston";
 import { z } from "zod";
 import { ApiResponse } from "./api-response";
 import { CommonConfig } from "./config-type";
-import { IOSchemaError, ResultHandlerError } from "./errors";
+import {
+  IOSchemaError,
+  OutputValidationError,
+  ResultHandlerError,
+} from "./errors";
 import {
   FlatObject,
   getActualMethod,
   getInput,
+  getMessageFromError,
   hasTopLevelTransformingEffect,
   makeErrorFromAnything,
 } from "./common-helpers";
@@ -217,20 +222,16 @@ export class Endpoint<
     try {
       return await this.outputSchema.parseAsync(output);
     } catch (e) {
-      if (e instanceof z.ZodError) {
-        throw new z.ZodError([
-          {
-            message: "Invalid format",
-            code: "custom",
-            path: ["output"],
-          },
-          ...e.issues.map((issue) => ({
-            ...issue,
-            path: issue.path.length === 0 ? ["output"] : issue.path,
-          })),
-        ]);
-      }
-      throw makeErrorFromAnything(e);
+      const error =
+        e instanceof z.ZodError
+          ? new z.ZodError(
+              e.issues.map(({ path, ...rest }) => ({
+                ...rest,
+                path: (["output"] as typeof path).concat(path),
+              }))
+            )
+          : makeErrorFromAnything(e);
+      throw new OutputValidationError(getMessageFromError(error));
     }
   }
 
diff --git a/src/errors.ts b/src/errors.ts
index d262f48d..7857c1ed 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -6,6 +6,8 @@ export class OpenAPIError extends Error {}
 
 export class IOSchemaError extends Error {}
 
+export class OutputValidationError extends IOSchemaError {}
+
 export class ResultHandlerError extends Error {
   protected readonly originalError?: Error;
 


Test patch:
diff --git a/tests/system/__snapshots__/system.spec.ts.snap b/tests/system/__snapshots__/system.spec.ts.snap
index 5420a1b4..45c178ac 100644
--- a/tests/system/__snapshots__/system.spec.ts.snap
+++ b/tests/system/__snapshots__/system.spec.ts.snap
@@ -39,7 +39,7 @@ exports[`App Validation Should fail on handler input type mismatch 1`] = `
 exports[`App Validation Should fail on handler output type mismatch 1`] = `
 {
   "error": {
-    "message": "output: Invalid format; anything: Number must be greater than 0",
+    "message": "output/anything: Number must be greater than 0",
   },
   "status": "error",
 }
diff --git a/tests/system/system.spec.ts b/tests/system/system.spec.ts
index 3af54ada..73efd3de 100644
--- a/tests/system/system.spec.ts
+++ b/tests/system/system.spec.ts
@@ -363,7 +363,7 @@ describe("App", () => {
           something: "gimme fail",
         }),
       });
-      expect(response.status).toBe(400);
+      expect(response.status).toBe(500);
       const json = await response.json();
       expect(json).toMatchSnapshot();
     });
diff --git a/tests/unit/endpoint.spec.ts b/tests/unit/endpoint.spec.ts
index 56479776..4dd9f381 100644
--- a/tests/unit/endpoint.spec.ts
+++ b/tests/unit/endpoint.spec.ts
@@ -182,11 +182,11 @@ describe("Endpoint", () => {
       const { responseMock } = await testEndpoint({
         endpoint,
       });
-      expect(responseMock.status).toBeCalledWith(400);
+      expect(responseMock.status).toBeCalledWith(500);
       expect(responseMock.json).toBeCalledWith({
         status: "error",
         error: {
-          message: "output: Invalid format; email: Invalid email",
+          message: "output/email: Invalid email",
         },
       });
     });
@@ -654,10 +654,10 @@ describe("Endpoint", () => {
       expect(responseMock.json).toHaveBeenCalledWith({
         status: "error",
         error: {
-          message: "output: Invalid format; output: failure on demand",
+          message: "output: failure on demand",
         },
       });
-      expect(responseMock.status).toHaveBeenCalledWith(400);
+      expect(responseMock.status).toHaveBeenCalledWith(500);
     });
   });
 
diff --git a/tests/unit/errors.spec.ts b/tests/unit/errors.spec.ts
index 18234b86..b44cb051 100644
--- a/tests/unit/errors.spec.ts
+++ b/tests/unit/errors.spec.ts
@@ -1,45 +1,53 @@
 import { DependsOnMethodError, OpenAPIError, RoutingError } from "../../src";
-import { IOSchemaError, ResultHandlerError } from "../../src/errors";
+import {
+  IOSchemaError,
+  OutputValidationError,
+  ResultHandlerError,
+} from "../../src/errors";
 
 describe("Errors", () => {
   describe("RoutingError", () => {
     test("should be an instance of Error", () => {
-      expect(new RoutingError("test") instanceof RoutingError).toBeTruthy();
-      expect(new RoutingError("test") instanceof Error).toBeTruthy();
+      expect(new RoutingError("test")).toBeInstanceOf(RoutingError);
+      expect(new RoutingError("test")).toBeInstanceOf(Error);
     });
   });
 
   describe("OpenAPIError", () => {
     test("should be an instance of Error", () => {
-      expect(new OpenAPIError("test") instanceof OpenAPIError).toBeTruthy();
-      expect(new OpenAPIError("test") instanceof Error).toBeTruthy();
+      expect(new OpenAPIError("test")).toBeInstanceOf(OpenAPIError);
+      expect(new OpenAPIError("test")).toBeInstanceOf(Error);
     });
   });
 
   describe("IOSchemaError", () => {
     test("should be an instance of Error", () => {
-      expect(new IOSchemaError("test") instanceof IOSchemaError).toBeTruthy();
-      expect(new IOSchemaError("test") instanceof Error).toBeTruthy();
+      expect(new IOSchemaError("test")).toBeInstanceOf(IOSchemaError);
+      expect(new IOSchemaError("test")).toBeInstanceOf(Error);
     });
   });
 
   describe("DependsOnMethodError", () => {
     test("should be an instance of RoutingError", () => {
-      expect(
-        new DependsOnMethodError("test") instanceof DependsOnMethodError
-      ).toBeTruthy();
-      expect(
-        new DependsOnMethodError("test") instanceof RoutingError
-      ).toBeTruthy();
+      expect(new DependsOnMethodError("test")).toBeInstanceOf(
+        DependsOnMethodError
+      );
+      expect(new DependsOnMethodError("test")).toBeInstanceOf(RoutingError);
+      expect(new DependsOnMethodError("test")).toBeInstanceOf(Error);
+    });
+  });
+
+  describe("OutputValidationError", () => {
+    test("should be an instance of IOSchemaError", () => {
+      expect(new OutputValidationError("test")).toBeInstanceOf(IOSchemaError);
+      expect(new OutputValidationError("test")).toBeInstanceOf(Error);
     });
   });
 
   describe("ResultHandlerError", () => {
     test("should be an instance of Error", () => {
-      expect(
-        new ResultHandlerError("test") instanceof ResultHandlerError
-      ).toBeTruthy();
-      expect(new ResultHandlerError("test") instanceof Error).toBeTruthy();
+      expect(new ResultHandlerError("test")).toBeInstanceOf(ResultHandlerError);
+      expect(new ResultHandlerError("test")).toBeInstanceOf(Error);
     });
 
     test(".hasOriginalError() should depend on original error", () => {