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

zlib: improve zlib errors #18675

Closed
wants to merge 2 commits into from

Conversation

joyeecheung
Copy link
Member

@joyeecheung joyeecheung commented Feb 9, 2018

zlib: improve zlib errors

  • Use assert to check mode in the Zlib constructor since it should
    only be passed by us.
  • Introduce checkRangesOrGetDefault() and checkFiniteNumber()
    to simplify type and range checking for numeric arguments
  • Instead of ERR_INVALID_OPT_VALUE, throw ERR_OUT_OF_RANGE and
    ERR_INVALID_ARG_TYPE with descriptions of the expected ranges
    or types to make the errors more user-friendly.
  • Add message tests for the changed errors
Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

zlib

@nodejs-github-bot nodejs-github-bot added the zlib Issues and PRs related to the zlib subsystem. label Feb 9, 2018
@joyeecheung
Copy link
Member Author

@joyeecheung joyeecheung added the errors Issues and PRs related to JavaScript errors originated in Node.js core. label Feb 9, 2018
Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

Nice work!

@@ -12,83 +12,201 @@ assert.ok(new zlib.Deflate() instanceof zlib.Deflate);
assert.ok(zlib.DeflateRaw() instanceof zlib.DeflateRaw);
assert.ok(new zlib.DeflateRaw() instanceof zlib.DeflateRaw);

// Throws if `opts.chunkSize` is invalid
// Throws if `optionss.chunkSize` is invalid
Copy link
Member

Choose a reason for hiding this comment

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

typo: options

@joyeecheung joyeecheung added semver-major PRs that contain breaking changes and should be released in the next major version. author ready PRs that have at least one approval, no pending requests for changes, and a CI started. labels Feb 10, 2018
Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

LGTM. Just two nits that are not blocking.

lib/zlib.js Outdated
!Number.isFinite(strategy))) {
throw new errors.TypeError('ERR_INVALID_ARG_VALUE', 'strategy', strategy);
if (validateFiniteNumber(level, 'level')) {
if (level < Z_MIN_LEVEL || level > Z_MAX_LEVEL) {
Copy link
Member

Choose a reason for hiding this comment

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

The statements could be combined. So it would only say:

if (validateFiniteNumber(level, 'level') && (level < ... || level > ...)) {
  // ...
}

The same for the one below.

Copy link
Member Author

@joyeecheung joyeecheung Feb 14, 2018

Choose a reason for hiding this comment

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

@BridgeAR That does not work because a valid value would be overwritten to the default in the else block.

Copy link
Member Author

Choose a reason for hiding this comment

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

@BridgeAR Ah..right, this is not one of the blocks that sets default values. Nevermind.

lib/zlib.js Outdated
if (chunkSize < Z_MIN_CHUNK) {
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'options.chunkSize',
`>= ${Z_MIN_CHUNK}`, chunkSize);
}
} else {
chunkSize = Z_DEFAULT_CHUNK;
}
Copy link
Member

Choose a reason for hiding this comment

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

Non blocking: A very personal point about readability: I feel like it would be better to write it as:

if (!validateFiniteNumber(...) {
  chunkSize = Z_...
} else if (chunkSize < Z_MIN_CHUNK) {
  throw new ...
}

@joyeecheung
Copy link
Member Author

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

LGTM, just needs a rebase and I left two suggestions.

lib/zlib.js Outdated
throw err;
}

if (!Number.isFinite(number)) {
Copy link
Member

Choose a reason for hiding this comment

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

Using Number.isFinite(number) will implicitly also test for NaN and for the type number. Therefore those two checks could be moved inside of this block to make the average case faster.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure I am following...NaN returns false here (no errors) while non-numbers throws a different error, how could we move those checks into this block?

Copy link
Member

@BridgeAR BridgeAR Feb 16, 2018

Choose a reason for hiding this comment

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

Oh, sorry, you can only move the typeof number !== 'number' in here. I overlooked that NaN should be accepted and gets a default value (do we really want that? I suggest to throw an error in that case as well).

The reason why the typecheck can be moved in here is:

Number.isFinite(nonNumber) === false. So the average case does not have to test for that. Only in case the isFinite test fails, it should be checked if it is the wrong type.

Copy link
Member Author

Choose a reason for hiding this comment

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

I also doubt if it's necessary to use default values for NaNs, but several tests fail if I change that so I would like to investigate this behavior later.

I get what you were suggesting now. That makes sense to me, I'll change that later, thanks

lib/zlib.js Outdated
} else if (flush < Z_NO_FLUSH || flush > Z_BLOCK) {
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'options.flush',
`>= ${Z_NO_FLUSH} and <= ${Z_BLOCK}`,
flush);
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: all cases that check for both bounds (that are all but one if I checked correct) could also be moved in a separate functions.

That way it would look like e.g.:

if (!validateFiniteNumber(flush, 'options.flush')) {
  arg = foo;
} else {
  validateRange(flush, Z_NO_FLUSH, Z_BLOCK, 'options.flush')
}

Or you actually combine the check and move the range check into the validateFiniteNumber:

if (!validateNumberRange(flush, Z_NO_FLUSH, Z_BLOCK, 'options.flush'))
  flush = Z_NO_FLUSH;

@BridgeAR BridgeAR removed the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Feb 16, 2018
- Use assert to check mode in the Zlib constructor since it should
  only be passed by us.
- Introduce checkRangesOrGetDefault() and checkFiniteNumber()
  to simplify type and range checking for numeric arguments
- Instead of `ERR_INVALID_OPT_VALUE`, throw `ERR_OUT_OF_RANGE` and
  `ERR_INVALID_ARG_TYPE` with descriptions of the expected ranges
  or types to make the errors more user-friendly.
- Add message tests for the changed errors
@joyeecheung
Copy link
Member Author

Rebased and updated. Introduced checkRangesOrGetDefault() that returns the number if it's valid, or the default if it's undefined or NaN, and throws appropriate errors otherwise. Put the range checks inside the Number.isFinite block for the common case.

CI: https://ci.nodejs.org/job/node-test-pull-request/13250/

@BridgeAR PTAL, thanks!

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

Still LGTM but I left a few suggestions. They are not blocking though.

lib/zlib.js Outdated
// 4. Throws ERR_OUT_OF_RANGE for infinite numbers or numbers > upper or < lower
function checkRangesOrGetDefault(number, name, lower, upper, def) {
if (checkFiniteNumber(number, name, lower, upper)) {
checkRanges(number, name, lower, upper);
Copy link
Member

Choose a reason for hiding this comment

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

checkRanges is only used here, so I guess it would be best to inline that.

}
flush = checkRangesOrGetDefault(
opts.flush, 'options.flush',
Z_NO_FLUSH, Z_BLOCK, Z_NO_FLUSH);
Copy link
Member

Choose a reason for hiding this comment

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

Hm, I just had a closer look and the values get initialized with their default value at the top of the function. So we can actually skip setting the variable again.

It is only necessary to set the variable in case a finite number got passed in. So I personally think it would still be best to have:

if (checkRange(opts.flush, 'options.flush', Z_NO_FLUSH, Z_BLOCK))
  flush = opts.flush;

But setting the value again is of course also not much overhead.

lib/zlib.js Outdated
// undefined or NaN
if (number === undefined || Number.isNaN(number)) {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

I would check for undefined first because it is the cheapest check and it is the most likely one (I guess the common use case is to rely on defaults).

So:

if (number === undefined)
  return false;
if (Number.isFinite(number)
  return true;
if (Number.isNaN(number)
  return false;

...

lib/zlib.js Outdated
// Infinite numbers
const err = new errors.RangeError('ERR_OUT_OF_RANGE', name,
'a finite number', number);
Error.captureStackTrace(err, checkFiniteNumber);
Copy link
Member

Choose a reason for hiding this comment

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

Right now this does not hide the checkRangesOrGetDefault frame anymore. This is problematic right now because the function is called from different functions and we can not just put in checkRangesOrGetDefault. I would probably just inline the checkFiniteNumber into checkRangesOrGetDefault and just have a bit of code copied for the single entry that does not fit into the pattern.

And it would be possible to combine this by doing:

let err;
if (typeif number !== 'number') {
  err = ...
} else {
  err = ...
}
Error.captureStackTrace(err, fn);
throw err;

@joyeecheung
Copy link
Member Author

Updated again, I don't feel too strongly about hiding all the frames or resetting the value so I left it there. @BridgeAR PTAL, thanks!

CI: https://ci.nodejs.org/job/node-test-pull-request/13280/

@joyeecheung joyeecheung added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Feb 22, 2018
@joyeecheung
Copy link
Member Author

Going to land this tomorrow...@BridgeAR

@joyeecheung
Copy link
Member Author

@joyeecheung
Copy link
Member Author

Landed in da886d9, thanks!

joyeecheung added a commit that referenced this pull request Feb 24, 2018
- Use assert to check mode in the Zlib constructor since it should
  only be passed by us.
- Introduce checkRangesOrGetDefault() and checkFiniteNumber()
  to simplify type and range checking for numeric arguments
- Instead of `ERR_INVALID_OPT_VALUE`, throw `ERR_OUT_OF_RANGE` and
  `ERR_INVALID_ARG_TYPE` with descriptions of the expected ranges
  or types to make the errors more user-friendly.
- Add message tests for the changed errors

PR-URL: #18675
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
MayaLekova pushed a commit to MayaLekova/node that referenced this pull request May 8, 2018
- Use assert to check mode in the Zlib constructor since it should
  only be passed by us.
- Introduce checkRangesOrGetDefault() and checkFiniteNumber()
  to simplify type and range checking for numeric arguments
- Instead of `ERR_INVALID_OPT_VALUE`, throw `ERR_OUT_OF_RANGE` and
  `ERR_INVALID_ARG_TYPE` with descriptions of the expected ranges
  or types to make the errors more user-friendly.
- Add message tests for the changed errors

PR-URL: nodejs#18675
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
author ready PRs that have at least one approval, no pending requests for changes, and a CI started. errors Issues and PRs related to JavaScript errors originated in Node.js core. semver-major PRs that contain breaking changes and should be released in the next major version. zlib Issues and PRs related to the zlib subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants