-
Notifications
You must be signed in to change notification settings - Fork 92
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
[SEMVER-MAJOR] Rework coercion of input arguments #343
Conversation
Cross-posting @STRML comment from #312 (comment)
Here is an example where the difference is important: let's say we have a method Having said that, I see your point of view @STRML. I think we can achieve what you are asking for by supporting a accepts: {
arg: 'numbers', type: ['number'], default: []
} Let's leave the support for default values out of scope of this patch though. |
Also: for long time, I though we can land (some of) these changes to 2.x in such way that backwards compatibility is preserved. I changed my mind after I checked the effect of #264 on the rest-coercion test suite, there are too many changes where I am concerned about the impact on existing applications. Our current plan is to release loopback@3 by the end of September, therefore I think it will be easier for all of us if this patch is landed on 3.0 only. |
I think the CI results cannot be trusted very much. First of all, this patch is targeting 3.0 release line, which is used in very few downstream projects. Secondly, |
@bajtos, |
losing precision. | ||
- Boolean types accepts only four string values: | ||
'true', 'false', '0' and '1' | ||
Values are case-insensitive, i.e. 'TRUE' and 'FaLsE' are work too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are work too
The review process could be much faster if this spread out into multiple commits. |
* return error ? { error: error } : { value: value }; | ||
* }, | ||
* | ||
* fromSloppyValue: function(ctx, value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a definition of a "sloppy value"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I took the concept of "sloppy" values from #264 but on the second thought the name is not descriptive enough.
The sloppy conversion/coercion is invoked when the value comes from a string-only source like HTTP header or more importantly from a query string parsed via qs
.
A sloppy value is one of:
- a string
- an array containing sloppy values
- an object where property values are sloppy
I am tempted to rename fromSloppyValue
to fromStringValue
, but that would hide the fact that an array or an object can be passed in too.
@ritch @richardpringle Any suggestions for a better name?
I reworked the comments in 0b7df4f to make this matter more clear, PTAL.
Removing |
* }); | ||
* ``` | ||
* | ||
* **Note: the alias `remotes.convert()` is deprecated.** | ||
* Under the hood, a converter is created that ensures the input data | ||
* is an object (or sloppy value is coerced to an object) and calls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sloppy value
I see this referenced in various places, and I am not sure what it means.
Does this mean the raw value provided from req.param()
or equiv?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #343 (comment)
The sloppy conversion/coercion is invoked when the value comes from a string-only source like HTTP header or more importantly from a query string parsed via qs.
Should I expand this comment to make it more clear what is meant by sloppy values?
Not really. We use |
assert(method && typeof method === 'object', | ||
'method must be a SharedClass instance'); | ||
assert(typeRegistry && typeof typeRegistry === 'object', | ||
'typeRegistry must be a TypeRegistry instance'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have used assert(...)
a lot in dao.js
as well; they are so neat; however I just wonder if the assertion fails, does it dump the stack trace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, failed assertions dump the stack trace. That way it's easy to find the place in user code that's calling ContextBase
with incorrect arguments.
if (this._options.warnWhenOverridingType !== false) | ||
g.warn('Warning: overriding remoting type %s', typeName); | ||
else | ||
debug('Warning: overriding remoting type %s', typeName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I love this ^ syntax; I mean skipping curly braces for one line if/else
, but is this what team follows? It reminds me of coffee script too :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we have a rule for that (yet). Since eslint
checks are passing, the style should be good.
TBH, I don't really mind. If you think I should add curly braces, then I'll do so. Please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats fine; TBH I like it too 👍 IIRC eslint used to be complaining about that.
@bajtos I reviewed almost one thirds of your pull request tonight; I added a bunch of comments, mostly questions and some suggestions. I'll try to review the rest tomorrow to unblock you :-) |
}, | ||
|
||
fromSloppyValue: function(ctx, value) { | ||
if (value === 'null' || value === null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: If value
is a string of null; is it always lowercase null
and not like NULL
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, only lowercase null
is recognized right now. Do you think this should be changed?
@Amir-61 you are awesome, thank you! |
assert(typeof typeName === 'string' && typeName, | ||
'typeName must be a non-empty string'); | ||
assert(typeof converter === 'object' && converter, | ||
'converter must be an object'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you just checking the type of converter
. In this case shouldn't be like the following?
assert(typeof converter === 'object',
'converter must be an object');
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, typeof null
is also object
:
> var converter = null
undefined
> typeof converter === 'object'
true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right...I missed that! 👍
Hey @bajtos, Please squash the commits and rebase them on top of master. |
Simplify the design of input argument handling. - It's up to the Context and/or the Adapter to convert or coerce input arguments as needed. - The only responsibility of SharedMethod is to validate input arguments (check that they match the contract in "accepts"). - Define two types of conversion/coercion: a) When the input argument value comes from a source that preserves type information (typically JSON), then the argument should be converted with minimal coercion. b) When the input argument value comes from a "sloppy" source (typically string-encoded source like a query string), then coercion rules should be applied. Replace "Dynamic" API, which was relying on global singleton registry, with a new concept of a "TypeRegistry" and a type "Converter". The new registry is local to each RemoteObjects instance. Each type converter provides three methods: - `fromTypedValue` to handle values coming from JSON - `fromSloppyValue` to handle values coming e.g. from query string - `validate` which is called by SharedMethod to validate argument values To prevent unnecessary try/catch blocks with the associated performance costs, validation/conversion errors are not thrown but returned via function return value instead. The feature where one could encode all input arguments in a single (query string) parameter "args" is removed. This patch fixes many subtle bugs and changes the way how edge case values are treated. The general approach is to make both conversion and coercion more strict. When we are not sure how to treat an input value, we rather return HTTP 400 Bad Request than coerce the value incorrectly. Most notable breaking changes: - `null` value is accepted only for "object", "array" and "any" - Empty string is coerced to undefined to support ES6 default arguments - JSON requests providing scalar values for array-typed argument are rejected - Empty value is not converted to an empty array - Array values containing items of wrong type are rejected. For example, an array containing a string value is rejected when the input argument expects an array of numbers. - Array items from JSON requests are not coerced. For example, `[true]` is no longer coerced to `[1]` for number arrays, and the request is subsequently rejected. - Deep members of object arguments are no longer coerced. For example, a query like `?arg[count]=42` produces `{ count: '42' }` now. - "any" coercion preserves too large numbers as a string, to prevent losing precision. - Boolean types accepts only four string values: 'true', 'false', '0' and '1' Values are case-insensitive, i.e. 'TRUE' and 'FaLsE' are work too. - Date type detects "Invalid Date" and rejects such requests. Last but not least, conversion/validation error messages no longer include input argument values, in order to prevent reflection-based attacks.
437eab4
to
6aaafd0
Compare
Simplify the design of input argument handling.
input arguments as needed.
arguments (check that they match the contract in "accepts").
a) When the input argument value comes from a source that preserves
type information (typically JSON), then the argument should be
converted with minimal coercion.
b) When the input argument value comes from a "sloppy" source
(typically string-encoded source like a query string), then
coercion rules should be applied.
Replace "Dynamic" API, which was relying on global singleton registry,
with a new concept of a "TypeRegistry" and a type "Converter".
The new registry is local to each RemoteObjects instance.
Each type converter provides three methods:
fromTypedValue
to handle values coming from JSONfromSloppyValue
to handle values coming e.g. from query stringvalidate
which is called by SharedMethod to validate argumentvalues
To prevent unnecessary try/catch blocks with the associated performance
costs, validation/conversion errors are not thrown but returned via
function return value instead.
The feature where one could encode all input arguments in a single
(query string) parameter "args" is removed.
This patch fixes many subtle bugs and changes the way how edge case
values are treated. The general approach is to make both conversion and
coercion more strict. When we are not sure how to treat an input value,
we rather return HTTP 400 Bad Request than coerce the value incorrectly.
Most notable breaking changes:
null
value is accepted only for "object", "array" and "any"rejected
example, an array containing a string value is rejected when
the input argument expects an array of numbers.
[true]
is no longer coerced to[1]
for number arrays,and the request is subsequently rejected.
a query like
?arg[count]=42
produces{ count: '42' }
now.losing precision.
'true', 'false', '0' and '1'
Values are case-insensitive, i.e. 'TRUE' and 'FaLsE' are work too.
Last but not least, conversion/validation error messages no longer
include input argument values, in order to prevent reflection-based
attacks.
@gunjpan @richardpringle @deepakrkris Please review.
@ritch @STRML PTAL too, I would like to hear your opinion on this change.
@0candy @davidcheung FYI
This patch is loosely inspired by #264.
The change-set is quite large and it may be too difficult to review all details. Here are the most important parts to check:
Connect to #312
Connect to strongloop-internal/scrum-loopback#974
Connect to strongloop-internal/scrum-loopback#885
TODO for @bajtos: