elysia icon indicating copy to clipboard operation
elysia copied to clipboard

Option to return errors as JSON

Open hassanshaikley opened this issue 2 years ago • 10 comments
trafficstars

Hey, it would be nice to be able to do that. I understand the onError callback but that requires a good bit of finessing to convert to JSON. In particular I am thinking of validator errors.

hassanshaikley avatar Nov 15 '23 13:11 hassanshaikley

I'm not sure if you mean or:

  1. Returning Error from handler as JSON
  2. Returning built-in Elysia errors like ValidationError, NotFound as JSON.

Either way, you should be able to use https://elysiajs.com/patterns/error-handling.html#catching-all-error

SaltyAom avatar Nov 16 '23 12:11 SaltyAom

what he means is for elysia to automatically send back the validation error as JSON not in plain text what should I do with this in my client side app? parse it? image

oSethoum avatar Nov 17 '23 15:11 oSethoum

Exactly @oSethoum!

If not automatically then it would be worth it to make it easier.

Automatically does make sense though, because going from JSON -> Text is super easy. But the other way around is much more tricky.

hassanshaikley avatar Nov 18 '23 16:11 hassanshaikley

import { Elysia, t } from "elysia";
import { PrismaClient } from "@prisma/client";

const db = new PrismaClient();

const app = new Elysia()
  .post(
    "/sign-up",
    async ({ body }) =>
      db.user.create({
        data: body,
      }),
    {
      body: t.Object({
        username: t.String(),
        password: t.String({
          minLength: 8,
        }),
      }),
    }
  )
  .onError(({ code, error }) => {
    switch (code) {
      case "VALIDATION":
        console.log(error.all);
        return error.all;
      default:
        return {
          name: error.name,
          message: error.message,
        };
    }
  })
  .listen(3000);

console.log(
  `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);

image

wiredmatt avatar Dec 09 '23 16:12 wiredmatt

I'm also looking for this support. At the moment, I can hack around it by adding this at the top of the Elysia chain:

.onError(({ error }) => {
	return {errors: [error.message]};
})

But this doesn't get handled nicely by Eden since it doesn't know that {error: string} will now be a valid response type from all API endpoints, so you end up needing to hack around the missing types on the client:

import { edenTreaty } from '@elysiajs/eden';
import type { App } from 'backend';

const api = edenTreaty<App>('api/');

const {data, error} = api.someendpoint.get();

// Eden doesn't know that `data` will return `{errors: Array<string>}` when a failure occurs :(
// So we have to add a type assertion
const typedData = data as (typeof data) | {errors: Array<string>}

if (error) {
    console.log("Failed due to", typedData.errors[0]);
}

Furthermore, onError doesn't seem to get called at all when using import {error} from 'elysia' mechanism. And I can't find any documentation about when it should be used.

EvHaus avatar Mar 11 '24 01:03 EvHaus

Ok, I think I have something more-or-less functional in Elysia 1.0.

Step 1

Create a custom Error object as per the docs. In my case I called it APIError. This is needed so you can pass custom values (like status codes) through to Elysia's onError function.

Mine looks like this:

import type { StatusCodes } from 'http-status-codes';

export class APIError extends Error {
  public readonly message: string;
  public readonly httpCode: StatusCodes;

  constructor(httpCode: StatusCodes, message: string, options?: ErrorOptions) {
    super(message, options);

    this.httpCode = httpCode;
    this.message = message;
    this.name = 'APIError';

    Object.setPrototypeOf(this, APIError.prototype);
    Error.captureStackTrace(this);
  }
}

Step 2

In your route handlers, throw these custom errors as needed. Example:

new Elysia().post('/myendpoint', async () => {
  if (somethingWentWrong) throw new APIError(422, 'Your custom error message here')
});

Step 3

In your global Elysia chain, add an onError handler that will intercept errors:

import type { ParseError, ValidationError } from 'elysia';

const errors = new Elysia()
  .error({ APIError })
  .onError({ as: 'global' }, ({ error, request, set, code }) => {
    logger.error(error);
    captureException(error);

    let message = 'Unknown error';
    set.status = StatusCodes.INTERNAL_SERVER_ERROR;

    switch (code) {
      case 'NOT_FOUND':
        set.status = StatusCodes.NOT_FOUND;
        message = `${request.method} ${request.url} is not a valid endpoint.`;
        break;
      case 'PARSE':
        set.status = error.status;
        message = error.message;
        break;
      case 'VALIDATION':
        set.status = error.status;
        // NOTE: You might want to change how these get returned to handle all possible typebox error states
        message = error.all[0].schema.error;
        break;
      case 'APIError':
        set.status = error.httpCode;
        message = error.message;
        break;
    }

    return Response.json(
      { error: message, otherstuff: 'whatever you want' },
      { status: set.status },
    );
  });

[!NOTE] Do not use import {error} from 'elysia'. It seems to bypass the onError handler.

Step 4

Seems to work fine in treaty as well, like so:

import { treaty } from '@elysiajs/eden';
import { app } from './index';

const api = treaty(app);
const { error, status } = await api.myendpoint.post();

console.log(error.value)
// => {error: 'Your custom error message here', otherstuff: 'whatever you want'}

Pitfalls

  1. Unfortunately error will be typed as unknown in this case. It looks like there should be a way to fix this, but my TypeScript skills are not good enough to figure out how. Any help would be appreciated.
  2. If you print error you get this ugly/useless thing: Error: [object Object]. It's not obvious that you have to use error.value.

EvHaus avatar Mar 19 '24 06:03 EvHaus

💎 ev is offering a $100 bounty for this issue 👉 Got a pull request resolving this? Claim the bounty by adding @algora-pbc /claim #313 in the PR body and joining algora.io

algora-pbc avatar Jul 06 '24 21:07 algora-pbc

All Elysia built-in errors should have been returning as JSON since ~0.7.0 by default

SaltyAom avatar Jul 23 '24 16:07 SaltyAom

I'll try to describe the request and problem more clearly.

Elysia's "Error Handling" docs provide this basic example:

const app = new Elysia()
    .onError(({ code, error }) => {
        return new Response(error.toString())
    })
    .get('/', () => {
        throw new Error('Server is during maintenance')
    })

This results in a request to / returning a 200 text response "Error: Server is during maintenance".

Let's change this to return a JSON error instead:

const app = new Elysia()
    .onError(({ code, error }) => {
        // With this change, I'm hoping Elysia will now return {failure: 'Server is during maintenance'}
        return {failure: error.message}
    })
    .get('/', () => {
        throw new Error('Server is during maintenance')
    })

Ok great! It works. A request to / now returns a 500 error with {"failure":"Server is during maintenance"}.

[!NOTE] Interestingly (but unrelated), a return {failure: error.message} will set the response status to 500, but return Response.json({failure: error.message}) will return a status of 200.

Now let's try to use this API from the client-side using Eden (as per the docs):

import { treaty } from '@elysiajs/eden'
import type { App } from '../src'

const app = treaty<App>('localhost:3000')
const { data, error } = await app.index.get();

if (error) { 
    // The desire is that `error` would have the type: `{failure: string}`
    console.log(error);
}

However, instead, the type is derived as:

{
  status: unknown;
  value: unknown;
} | null

You can also see this on the Elysia docs website itself too:

image

Instead, I'm expecting error to have the type:

{
  failure: string
}

EvHaus avatar Jul 24 '24 05:07 EvHaus

Any update on this? I really don't like having status and value being of type unknown when their structure is known. Any one know of a way to cast the error in each request? Can it somehow be casted globally when instantiating the Eden client?

0xd8d avatar Aug 08 '24 06:08 0xd8d