fastify-type-provider-zod icon indicating copy to clipboard operation
fastify-type-provider-zod copied to clipboard

Zod errors messages are weirdly formatted

Open elie-h opened this issue 2 years ago • 10 comments

I noticed validation errors are wierdly formatted, is this just a case of having to parse at the client or is it a bug?

For example: I have the following zod object:

  address: z
    .string()
    .refine((val) => val == "12345", {
      message: "This is an error message",
    })

This is the error I get, the message is weirdly formatted with new lines:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "[\n  {\n    \"code\": \"custom\",\n    \"message\": \"This is an error message\",\n    \"path\": [\n      \"address\"\n    ]\n  }\n]"
}

Any ideas?

elie-h avatar Nov 21 '22 14:11 elie-h

It is fastify that stringifies the error, but there is a way to make it cleaner. I will take a look at that.

turkerdev avatar Nov 21 '22 15:11 turkerdev

Was looking to see if that can be disabled, but couldn't find a way to hook into it at the code level.

elie-h avatar Nov 21 '22 15:11 elie-h

@eh-93 You can implement custom errorHandler on fastify level to format your errors

kibertoad avatar Nov 21 '22 15:11 kibertoad

I meant at the library level. The logic to determine whether the message needs formatting or not would get really messy and probably slow down the req/res cycle. I think this is an issue of consistency as opposed to custom error handling.

Not all error messages are stringified, for example if you return an error in the handler:

        reply.status(404).send({
          error: "Not found",
          message: "Not Found",
        });

The message in the response body isn't stringified and is missing the status code that would be present in the case of the error being thrown at the zod validation stage:

{
  "error": "Not found",
  "message": "Not Found"
}

I've added a custom errorHandler like so:

  app.setErrorHandler((error, request, reply) => {
    if (error instanceof ZodError) {
      console.log("Error me out", JSON.parse(error.message).message);
      reply.status(400).send({
        message: "Validation error",
        errors: JSON.parse(error.message),
      });
    }
  });

That works fine functionally, but not ideal because that error schema never makes it to swagger

elie-h avatar Nov 21 '22 15:11 elie-h

We can remove the newlines(\n) from the stringified message but looks like you want the message to be a JSON object, I don't think we can interfere it at validation time.

turkerdev avatar Nov 21 '22 15:11 turkerdev

I was just tinkering, Stripping the new lines so that it's just a message would help.

I've now hooked into the error handler and have managed to do that at the fastify level similar to the example above. But the downside of that is that the error schema isn't reflected in the generated OpenAPI doc.

elie-h avatar Nov 22 '22 14:11 elie-h

to followup on @eh-93 workaround, you do not need to do the JSON.parse, the parsed error is already there

app.setErrorHandler((error, request, reply) => {
  if (error instanceof ZodError) {
    reply.status(400).send({
      statusCode: 400,
      error: "Bad Request",
      issues: error.issues,
    });
    return;
  }

 reply.send(error);
})

Hurtak avatar Apr 20 '23 11:04 Hurtak

Don't forget do return

  server.setErrorHandler((error, request, reply) => {
    if (error instanceof ZodError) {
      reply.status(400).send({
        statusCode: 400,
        error: 'Bad Request',
        message: 'Validation error',
        errors: JSON.parse(error.message),
      });
      return;
    }

    reply.send(error);
  });

vnva avatar Oct 21 '23 16:10 vnva

Thank you @elie-h and @Hurtak !

One additional thing that still frustrated me about these error responses is that they don't tell you which part of the request failed the schema validation; querystring, body, headers, params?

I found that by customizing the validator compiler a bit you can pass it down to the error handler.

import {type FastifyInstance} from 'fastify'
import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod'
import {FastifyRouteSchemaDef} from 'fastify/types/schema'
import {ZodAny, ZodError} from 'zod'

async function registerZod(fastify: FastifyInstance) {
  fastify.setValidatorCompiler((req: FastifyRouteSchemaDef<ZodAny>) => {
    return (data: any) => {
      const res = validatorCompiler(req)(data)
      // @ts-ignore
      if (res.error) res.error.httpPart = req.httpPart
      return res
    }
  })

  fastify.setSerializerCompiler(serializerCompiler)

  // The default zod error serialization is awful
  // See: https://github.com/turkerdev/fastify-type-provider-zod/issues/26#issuecomment-1322254607
  fastify.setErrorHandler((error, request, reply) => {
    if (error instanceof ZodError) {
      reply.status(400).send({
        statusCode: 400,
        error: 'Bad Request',
        // @ts-ignore -- This is set in the validator compiler above 👆
        httpPart: error.httpPart,
        issues: error.issues,
      })
      return
    }
    reply.send(error)
  })
}

0livare avatar Jun 07 '24 01:06 0livare

An improvement with TypeScript integration.

// src/config/config.type.ts
import "zod"

declare module "zod" {
  interface ZodError {
    httpPart?: string
  }
}

// src/config/config.service.ts
function registerZod(server: FastifyInstance) {
  server.setValidatorCompiler((req: FastifyRouteSchemaDef<ZodAny>) => {
    return async (data: unknown) => {
      const res = await validatorCompiler(req)(data)
      if (res.error) res.error.httpPart = req.httpPart
      return res
    }
  })

  server.setSerializerCompiler(serializerCompiler)

  server.setErrorHandler(
    (error: FastifyError, _: FastifyRequest, reply: FastifyReply) => {
      if (error instanceof ZodError)
        reply.status(status.BAD_REQUEST).send({
          statusCode: status.BAD_REQUEST,
          error: "Bad Request",
          httpPart: error.httpPart,
          issues: error.issues,
        })
      reply.send(error)
    },
  )
}

alnah avatar Aug 31 '24 17:08 alnah