-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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(rest): switch to body parser and add extensibility for parsers #1936
Conversation
404bdcc
to
8625673
Compare
@raymondfeng what's the status of this pull request, is it ready for review? I see a conflict in |
8625673
to
8c147af
Compare
@bajtos I resolved the conflict and it's ready for review. |
5c2051d
to
98f6caf
Compare
@bajtos PTAL |
Sorry, the GitHub notification got buried in my inbox, I'll review early next week. |
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.
Besides the comments below, the new extension points (custom body parsers, x-skip-body-parsing
) need a documentation.
It would be also great to have a new example app showing a reference implementation of file uploads, ideally with a simple HTML5 front-end that we can open in browser to play with this functionality. I think that's out of scope of this pull request though.
await client | ||
.post('/products') | ||
.type('json') | ||
.expect(422); |
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.
With this change in place, the comment at the top of the test is inconsistent with what is the test asserting. Please take a closer look, I would like us to understand how are empty payloads handled with the new request handler in place.
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.
PTAL ☝️
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.
Still not addressed ☝️
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.
❗️ Is this change in the test hinting at breaking backwards compatibility?
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.
It turns out that express json body parser creates {}
for empty body. I added some logic in json parser to treat empty body as undefined
.
c38156f
to
af27150
Compare
@bajtos PTAL |
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 love the new file structure, it makes the code so much easier to read and navigate around.
The code in individual built-in parser classes seems to be duplicated a lot, have you considered using a Strategy or Template design pattern to extract a shared base class?
I run out of time while reviewing the changes and did not manage to review the changes in test files yet. Sorry for that.
'rest.requestBodyParser', | ||
); | ||
|
||
export const REQUEST_BODY_PARSER_JSON = BindingKey.create<BodyParser>( |
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.
Is it a good idea to turn binding keys of individual parsers into a public API? Personally, I would expect that these keys should be treated as an implementation detail, similarly to how we are not making much guarantees about the binding keys for controllers, repositories and routes.
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.
@raymondfeng you haven't addressed nor responded to this comment. PTAL again.
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.
Let's discuss this point before landing the pull request. If we wanted to remove these public keys later, then we would have to wait until we are ready for a semver-major version.
await client | ||
.post('/products') | ||
.type('json') | ||
.expect(422); |
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.
PTAL ☝️
@raymondfeng I would like you to consider the following high-level scenarios, I am not sure if the current design for body-parser registration is flexible enough to support them. While the canonical content-type for JSON payloads is This scenario came to my attention because I incorrectly assumed that It opens a more generic question of the priority of body parsers. Let's say the user has two parsers, one recognizing Do we need to sort the parsers from most-specific to most-generic? To support all JSON-based content-types, should we broaden content types accepted by our built-in parser to allow any of |
af27150
to
4bfc7ab
Compare
@bajtos We can possibly use the http
Such features can be done outside of this PR. Please note that http content negotiation utilizes the |
1030d7d
to
9533889
Compare
I am not sure if that will work, to be honest.
I am fine to leave it outside of this PR, but I am concerned that before we do at least some minimal implementation, then we will not know whether the proposed approach is actually feasible :( Can we at least put the built-in parsers to the end of the list, so that user-defined parsers always take precedence? It may be enough to sort the list of injected parsers in reverse, so that the last parser registered will be the first parser to have a chance to process the request. |
I was thinking about this problem more and have another idea to consider. In Express, middleware preprocessing requests (e.g. parsing the body) does not have any route-specific metadata to use as a guide to decide which behavior to use (which parser to execute). In LoopBack 4, routes provide OpenAPI spec which is allows us to provide additional instructions for body parsing (accepted content types + extra metadata for each type, e.g. schema). So I was thinking about the following solution, which will cover both selection of the right parser and the ability to skip body parsing.
Few examples: // parse text/json as JSON
@requestBody({
content: {
'text/json': {
'x-parser': 'json',
schema: {/*...*/},
},
}
})
// parse custom type as text
@requestBody({
content: {
'text/calendar': {
'x-parser': 'text',
schema: {/*...*/},
},
},
})
// consume unsupported type as a stream (skip body parsing)
@requestBody({
content: {
'multipart/form-data': {
'x-parser': 'stream',
schema: {(/*...*/},
},
},
});
// override handling of a known type - consume the body as a stream
@requestBody({
content: {
'application/json': {
'x-parser': 'stream',
schema: {(/*...*/},
},
},
});
// provide a custom parser class
@requestBody({
content: {
'multipart/form-data': {
'x-parser': FileUploadParser,
schema: {(/*...*/},
},
},
});
class FileUploadParser implements BodyParser {
// ...
} I find this approach very elegant, because we can leverage the same mechanism ( We can even push this approach one step further and always use "stream" (no-op) body parser for content types that don't have any matching parser. I.e. introduce a new default behavior where bodies with unsupported content types are automatically passed to controller methods/route handlers as streams. This is based on the assumption that our body parsing step is accepting only the content types described in the OpenAPI spec, and returning "Unsupported Media Type" for requests with other content types. @raymondfeng @strongloop/loopback-next thoughts? |
096cad4
to
087e157
Compare
@bajtos I have implemented the |
60b7585
to
03b9be4
Compare
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 am glad you liked my x-parser
proposal, I think the updated implementation looks much better.
Besides the comments I am leaving below, I am missing tests for the following scenarios/cases:
Acceptance-level tests at RestApplication level showing how to:
-
Show how to configure the app to parse all incoming
text/json
requests using JsonBodyParser. -
Override a built-in body parser, e.g. configure a custom parser to handle
application/json
type for all incoming requests. -
Disable a built-in body parser, e.g. configure the app to always reject url-encoded requests, even if the spec allows that media type. (Maybe this isn't very useful and there is no need for such test?)
Specifically, these tests must not use x-parser
extension.
Further scenarios, I think they are more suited for integration tests of unit tests:
- What happens when a custom parser returns
undefined
. - How is each built-in parser dealing with an empty request body. Does it throw 400, pass
undefined
or an empty string, something else? - When a custom parser throws an error, it should be normalized consistently with errors reported by built-in parser.
- A test showing why we need error normalization for built-in parsers, i.e. setup a scenario where a built-in parser returns a "wrong" error and we need to normalize it.
Documentation wise, I am missing guide for extension developers showing them how to write an extension contributing custom parsers. I am ok to leave this out of this PR as long as you create a follow-up issue.
Cross-posting a part of one of the line comments that I consider as still relevant: It would be even more cool if we could have two acceptance tests for file upload:
|
@raymondfeng To make my further reviews easier, please don't amend any existing commits and push new commits only. (Rebasing on top of master is fine.) That way I can review only new increments and don't have to re-read all ~1.6k changed lines again. Considering the scope of the changes proposed in this pull request, I am not sure if it still makes sense to preserve the current series of smaller commits. If it's easier for you to stop splitting the changes into small commits, with the vision of landing this entire PR in one or two big commits, then feel free to do so. (As long as I have an easy way how to see what has changed since my last review.) |
8f444db
to
922fc77
Compare
See |
@bajtos PTAL. |
'rest.requestBodyParser', | ||
); | ||
|
||
export const REQUEST_BODY_PARSER_JSON = BindingKey.create<BodyParser>( |
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.
@raymondfeng you haven't addressed nor responded to this comment. PTAL again.
this.restServer.bodyParser(bodyParserClass); | ||
bodyParser( | ||
bodyParserClass: Constructor<BodyParser>, | ||
address?: BindingAddress<BodyParser>, |
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.
Can we use key
instead of address
for consistency with other places please? Most notably Context
class uses key
in most of places.
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 just checked. The Context
class BindingAddress
for most cases.
If you refer to app.controller(controllerClass, name)
or similar methods, I intentionally make it different so that we can use the well-know binding address to override built-in body parsers.
BTW, export const REQUEST_BODY_PARSER_JSON
in keys.ts
is by design to support the acceptance test of overriding built-in body parsers.
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 agree we should use BindingAddress<BodyParser>
type, but I am proposing to call the argument key
, not address
.
I was referring to Context APIs like ctx.bind
, ctx.contains
, ctx.unbind
and so on.
https://github.com/strongloop/loopback-next/blob/61045e7540ff2f8c41db5991e91f8c0ec77555f5/packages/context/src/context.ts#L102
https://github.com/strongloop/loopback-next/blob/61045e7540ff2f8c41db5991e91f8c0ec77555f5/packages/context/src/context.ts#L88
https://github.com/strongloop/loopback-next/blob/61045e7540ff2f8c41db5991e91f8c0ec77555f5/packages/context/src/context.ts#L47-L49
packages/rest/test/acceptance/file-upload/file-upload-with-parser.acceptance.ts
Outdated
Show resolved
Hide resolved
packages/rest/test/acceptance/request-parsing/request-parsing.acceptance.ts
Outdated
Show resolved
Hide resolved
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.
Looks better now.
Please take a look at the older comments, there are many of them that are not resolved yet. I am open to discussion on which of the comments must be made before landing and which should be left out of scope, the ball is on your side.
Also: Documentation wise, I am (still) missing a guide for extension developers showing them how to write an extension contributing custom parsers.
922fc77
to
4c2e033
Compare
Yes. It's fixed.
|
Do you think we need to refactor this section to a separate guide? https://github.com/strongloop/loopback-next/blob/switch-to-body-parser/docs/site/Parsing-requests.md#adding-parsers |
4c2e033
to
e125072
Compare
@bajtos I believe that I have addressed most of your comments. PTAL. If you think there are more blocking issues, let me know. |
@bajtos ping. |
Eventually, I think we will need to split "Parsing requests" into multiple pages, because it's growing too long. BUT: As far as this PR is concerned, I am fine with keeping everything in a single page. When I put myself in the shoes of an extension developer, I see the following shortcomings:
Here is what I have in mind:
|
@raymondfeng Here is the list of comments where I feel we did not reach consensus yet and where it would be difficult to change the code/APIs later:
|
d8ebc98
to
2eeee63
Compare
@bajtos I have addressed all of your pending comments. PTAL. |
eaf80f5
to
3078aea
Compare
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.
LGTM 👍
The new doc page "Extending-request-body-parsing" is missing instructions on how a component can contribute a new/custom parser binding.
Explain how to wire up the extension (a Component class) so that the custom parser is picked up by target applications. Include a code snippet showing how to use
bodyParserBindingKey
helper to create the binding.
- add `bodyParser` sugar method - use `x-parser` to control custom body parsing - add docs for body parser extensions - add raw body parser
3078aea
to
bf12cae
Compare
Added. |
Yay 🕺 🎉 |
Recreation of #1887
This PR adds the following features:
switches to express body-parser:
Allow body parsers to be plugged in
Checklist
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updatedDocumentation TODO as seen by @bajtos:
createBodyParserBinding
).executeBodyParserMiddleware
, this function can be very useful to extension authors building custom body parsers, because it effectively allows them to take any Express middleware performing body parsing and wrap it into LB4 RequestParser. It would be great to change it into a well-documented public API.@requestBody
overview.