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

feat(common): add error options object #10460

Conversation

thiagomini
Copy link
Contributor

add error options object to HttpException constructor to allow use of error cause along with custom message.

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

We can only pass either a custom description or a cause error object.

Issue Number: #10392

What is the new behavior?

We can now use both custom messages and error cause with the following API:

const error = new HttpException('customDescription', 400, {
        cause: new Error(),
      });

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

We have yet to define what is the expected behavior when someone mistakenly passes both the first argument as an Error and also the options object with a cause. In that case, should we always consider the cause property or throw an error, @kamilmysliwiec ?

add error options object to HttpException constructor to allow use of error cause along with custom message.
Comment on lines +20 to +26
* throw new HttpException()
* throw new HttpException('message', HttpStatus.BAD_REQUEST)
* throw new HttpException({ reason: 'this can be a human readable reason' }, HttpStatus.BAD_REQUEST)
* throw new HttpException(new Error('Cause Error'), HttpStatus.BAD_REQUEST)
* throw new HttpException('custom message', HttpStatus.BAD_REQUEST, {
* cause: new Error('Cause Error'),
* })
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a bunch of examples to better illustrate the possibilities here.

@coveralls
Copy link

coveralls commented Oct 25, 2022

Pull Request Test Coverage Report for Build 47ec2c16-536f-4fcd-8acd-7e7f64581b60

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.02%) to 93.804%

Totals Coverage Status
Change from base Build ee4c81ca-3d86-452b-91d8-8e84e7b63778: 0.02%
Covered Lines: 6116
Relevant Lines: 6520

💛 - Coveralls

this.cause = this.options.cause;
return;
}

if (this.response instanceof Error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have yet to define what is the expected behavior when someone mistakenly passes both the first argument as an Error and also the options object with a cause. In that case, should we always consider the cause property or throw an error, @kamilmysliwiec ?

Following up on this ☝️ here.

Since we now have a dedicated options object, perhaps we can just log the warning down when this.response instanceof Error === true saying that from now on, the { cause: ... } option should be used instead (and the previous syntax is deprecated)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we could do that, but then this would be a breaking change to the users that are already using the first parameter as the error cause. Is it okay to break this interface and just log a warning?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thiagomini the idea is to log the warning now and introduce a breaking change as part of the next major release

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm I see, alright then, I'll keep both options and just log the deprecated message.

add deprecated warning for HttpException class when using the first argument as the error cause.
add error cause option to bad request exception.
change BadRequestException constructor parameters assignment to be clearer.
expect(cause).to.be.eql(causeError);
});

it('configures a cause when using a built-in exception with options', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a POC for how we could use the options object with other built-in exceptions, like the BadRequestException. The modification here, as you can see in the class implementation, was to make the second argument either an object or a string. When it's an object, then we consider it as an option, otherwise, we consider it to be the old description parameter. @kamilmysliwiec , what do you think about that interface?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great!

description = 'Bad Request',
descriptionOrOptions:
| string
| (HttpExceptionOptions & { description?: string }) = 'Bad Request',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline type here is provisory. I may change it as soon as we validate an interface for these errors.

rearrange HttpException spec file test blocks to increase readability, better separating tests by methods and contexts.
rename HttpException createBody method parameter to better represent its usage. When this parameter is a string, it is used as a value to a 'message' key in the final object.
add test for HttpException's getResponse method when used with a built-in exception and providing the "description" parameter as part of the "options" object.
};
expect(new HttpException(message, 404).getResponse()).to.be.eql(message);
});
describe('getResponse', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, I decided to group the related tests together under a common describe block. Many tests were actually testing the result for getResponse() method, while others were related to serialization. This aggregation made it easier for me to understand what was being tested. If you have strong feelings against it @kamilmysliwiec , let me know and I can revert it. However, this separation helped me notice there was a duplicated test for serialization. the test under the otherwise describe block was basically the same as the it('should be serializable', () => { }. So, in conclusion, I think this was a positive change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

@@ -112,15 +112,16 @@ export class HttpException extends Error {
}

public static createBody(
objectOrError: object | string,
objectOrErrorMessage: object | string,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed this parameter to reflect its usage better. When this is a string, it's used as the message attribute below, so this new name seemed a better fit.

Comment on lines +64 to +66
const badRequestError = new BadRequestException('ErrorMessage', {
description: 'Some error description',
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test exemplifies how we can use the description parameter inside the options object. Before my changes, the BadRequestException second argument was a simple string for the error's description. With the addition of the cause attribute, I had to put them both under the same object literal.

add description attribute to HttpExceptionOptions interface so we can reuse it among children exceptions.
add error cause option to BadGatewayException.
extract functions that provide valid values for description and http exception options to the HttpException class
create HttpException#extractDescriptionAndOptionsFrom method to return both description and options at the same time
add docs for HttpException.getHttpExceptionOptionsFrom explaining its intended usage.
test error cause option for BadRequestException.
aggregate HttpException children classes tests in a single run to reduce the code duplication
add error cause option to ConflictException.
update HttpException children classes docs to reflect the changes to the "description" parameter
add error cause option to ForbiddenException.
add error cause option to GatewayTimeoutException.
add error cause option to GoneException.
add error cause option to HttpVersionNotSupportedException.
add error cause option to ImATeapotException.
add error cause option to InternalServerErrorException.
add error cause option to MethodNotAllowedException.
add error cause option to MisdirectedException.
add error cause option to NotAcceptableException.
add error cause option to NotFoundException.
add error cause option to NotImplementedException.
add error cause option to PayloadTooLargeException.
add error cause option to PreconditionFailedException.
add error cause option to RequestTimeoutException.
add error cause option to ServiceUnavailableException.
add error cause option to UnauthorizedException.
add error cause option to UnprocessableEntityException.
add error cause option to UnsupportedMediaTypeException.
reuse errorCause variable in HttpException test
add tests for all HttpException children classes and fix NotAcceptableException wrong status code.
add "getStatus" tests for all HttpException children classes
describe('built-in exceptions', () => {
describe('getStatus', () => {
it('should return given status code', () => {
const testCases: [Type<HttpException>, number][] = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to add tests here to guarantee the changes I've made didn't break anything. Fortunately, this really prevented me from breaking NotAcceptableException that I mistakenly changed its status code.


describe('getResponse', () => {
it('should return a response with default message and status code', () => {
const testCases: [Type<HttpException>, number, string][] = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests for all built-in exceptions just to make sure everything is still working.

@thiagomini
Copy link
Contributor Author

Hey, @kamilmysliwiec , you might want to review this commit-by-commit now that the number of files changed increased a lot.

@kamilmysliwiec
Copy link
Member

LGTM

@kamilmysliwiec kamilmysliwiec merged commit 7ec60ca into nestjs:master Nov 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants