Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to customize the JSON content of HTTP error responses #1867

Closed
hbakhtiyor opened this issue Oct 17, 2018 · 8 comments
Closed

How to customize the JSON content of HTTP error responses #1867

hbakhtiyor opened this issue Oct 17, 2018 · 8 comments
Labels
Docs REST Issues related to @loopback/rest package and REST transport in general user request

Comments

@hbakhtiyor
Copy link

hbakhtiyor commented Oct 17, 2018

for example

// src/controllers/hello.controller.ts
import {HelloRepository} from '../repositories';
import {HelloMessage} from '../models';
import {get, param, HttpErrors} from '@loopback/rest';
import {repository} from '@loopback/repository';

export class HelloController {
  constructor(@repository(HelloRepository) protected repo: HelloRepository) {}

  // returns a list of our objects
  @get('/messages')
  async list(@param.query.number('limit') limit = 10): Promise<HelloMessage[]> {
    // throw an error when the parameter is not a non-positive integer
    if (!Number.isInteger(limit) || limit < 1) {
      throw new HttpErrors.UnprocessableEntity('limit is non-positive');
    } else if (limit > 100) {
      limit = 100;
    }
    return await this.repo.find({limit});
  }
}

invalid fields will result in a 422 unprocessable entity response with json body.

{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Hello",
      "field": "limit",
      "code": "non_positive"
    }
  ]
}
@raymondfeng
Copy link
Contributor

@hbakhtiyor Do you expect the error thrown by the controller code to override our built-in validation?

@jannyHou
Copy link
Contributor

@raymondfeng I think he's asking after applying his own validation

if (!Number.isInteger(limit) || limit < 1) {
  // how to custom the response here
}

how to custom the response like https://github.com/strongloop/loopback-next/pull/1753/files#diff-3497d60dccb2aa1d1fdf2955a7a080e9R239

@raymondfeng
Copy link
Contributor

I don't think the controller method is invoked at all as the validation of limit already fails before we route to the controller implementation.

@hbakhtiyor
Copy link
Author

hbakhtiyor commented Oct 18, 2018

@jannyHou yeah, @raymondfeng but the response matters, need in json and the structure

@bajtos bajtos changed the title how to throw custom validation errors back to client in json response? How to customize the JSON content of HTTP error responses Oct 30, 2018
@bajtos bajtos added Docs REST Issues related to @loopback/rest package and REST transport in general labels Oct 30, 2018
@bajtos
Copy link
Member

bajtos commented Oct 30, 2018

@hbakhtiyor Building HTTP error responses is a tricky business. It's easy to get it wrong and open your application to attacks.

In LoopBack (both 3.x and 4.x), we use our strong-error-handler middleware to take care of this. See Handling Errors in our docs.

Here are the important security constraints to keep in mind:

In production mode, strong-error-handler omits details from error responses to prevent leaking sensitive information:

  • For 5xx errors, the output contains only the status code and the status name from the HTTP specification.
  • For 4xx errors, the output contains the full error message (error.message) and the contents of the details property (error.details) that ValidationError typically uses to provide machine-readable details about validation problems. It also includes error.code to allow a machine-readable error code to be passed through which could be used, for example, for translation.

In debug mode, strong-error-handler returns full error stack traces and internal details of any error objects to the client in the HTTP responses.

Now that I have warned you, LoopBack 4 makes it very easy to format the error messages your way. Just provide a custom implementation of the Sequence action reject. See Customizing Sequence Actions in our docs, it explain how to create a custom send action. The solution for reject is pretty much the same, you just need a different signature for the action function.

export class CustomRejectProvider implements Provider<Reject> {
  // ...
  action({request, response}: HandlerContext, error: Error) {
    // handle the error and send back the error response
    // "response" is an Express Response object
  }
}

Caveat: some errors thrown by LB4 have only code set, these errors need a bit of pre-processing to decide what HTTP status code they should trigger. (For example, the error code ENTITY_NOT_FOUND should be mapped to the status code 404). The built-in reject action does not yet expose this pre-processing for consumption by custom reject actions. It's an oversight on our side, l created a new issue #1942 to keep track of that.

@shadyanwar
Copy link

shadyanwar commented Jan 21, 2019

@bajtos I have attempted making a custom error provider which takes two arguments: an array of errors and statusCode based on your comment. Here is the code I used:

export class CustomRejectProvider implements Provider<Reject> {
  constructor(
    @inject('customErrors') public errors: Array<Object>,
    @inject('customErrorCode') public stathsCode: number,
  ) { }

  value() {
    // Use the lambda syntax to preserve the "this" scope for future calls!
    console.log("test1");
    return (response: HandlerContext, result: Error) => {
      this.action(response, result);
    };
  }

  action({ request, response }: HandlerContext, error: Error) {
    // handle the error and send back the error response
    // "response" is an Express Response object
    console.log("test2");
    if (error) {
      console.log("test3");
      error.message = 'Message: my error';
      error.name = 'Name: some error';
      const headers = (request.headers as any) || {};
      const header = headers.accept || 'application/json';
      response.setHeader('Content-Type', header);
      response.sendStatus(401);
      response.end(error);
    } else {
      console.log("test4");
      response.end(response);
    }
  }

Response:

{
    "errors": [{
        "msg": "first element"
    }, {
        "msg": "second element"
    }],
    "statusCode": 401
}

The response however contains only the two injected variables (the error array and the statusCode) exactly as provided to the class without any processing. All the console log statements in my code have no effect at all. Any tips? I was also unable to modify the statusCode of the response. It's always 200 OK.

@pktippa
Copy link
Contributor

pktippa commented Oct 19, 2019

@shadyanwar
Replace

response.sendStatus(401);
response.end(error);

with

response.status(401).send(error);

and try, it worked for me.

@dhmlau
Copy link
Member

dhmlau commented Mar 27, 2020

Thanks @shadyanwar @pktippa. I've created PR based on the above suggestions. See https://github.com/strongloop/loopback-next/pull/4969/files.

@hbakhtiyor, we believe we've addressed your question. Closing as done. Please feel free to open a new issue if you find other problems. Thanks.

@dhmlau dhmlau closed this as completed Mar 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Docs REST Issues related to @loopback/rest package and REST transport in general user request
Projects
None yet
Development

No branches or pull requests

7 participants