turkerdev / fastify-type-provider-zod

MIT License
337 stars 21 forks source link

Zod errors messages are weirdly formatted #26

Open elie-h opened 1 year ago

elie-h commented 1 year ago

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?

turkerdev commented 1 year ago

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

elie-h commented 1 year ago

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

kibertoad commented 1 year ago

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

elie-h commented 1 year ago

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

turkerdev commented 1 year ago

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.

elie-h commented 1 year ago

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.

Hurtak commented 1 year ago

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);
})
vnva commented 10 months ago

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);
  });
0livare commented 3 months ago

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)
  })
}
alnah commented 6 days ago

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)
    },
  )
}