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

[RFC] feat: use express/koa/... as the http framework #1082

Closed
wants to merge 1 commit into from

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented Mar 5, 2018

This PR might be a bit controversial.

Connect to #1071.

Key incentives:

  1. Introduce HttpContext object to wrap request/response of some flavors (such as Node core http, Express, Koa, or our home grown). In this PR, we directly use Express Request/Response as the http interfaces.

  2. Extract the logic to create (REST) servers into http-server.ts and provide a reference implementation using Express (only the http core, not routing) - it should be possible to refactor the common HTTP logic into a separate module (@loopback/http-server) so that similar stacks such as Koa, Hapi, or Restify can be potentially plugged in.

Pros:

  1. We are Express compatible again.
  2. We get the common http capabilities including middleware from Express and its ecosystem for free.
  3. Different http frameworks can be plugged into LoopBack 4.

Cons:

  1. Dependency on Express for the REST module and it's a bit more opinionated.

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • Related API Documentation was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in packages/example-* were updated

@bajtos
Copy link
Member

bajtos commented Mar 5, 2018

I'll take a look when I get back from my vacation (not sooner than in two weeks).

@bajtos
Copy link
Member

bajtos commented Mar 5, 2018

My main problem with Express is its concept of the next callback that's not called when the request is handled. When next is not called, express skips any middleware/sequence actions that should have been executed after the middleware/route.

So far, we based the design of our REST layer on the assumption that each sequence action (a middleware) always returns back to the framework, so that subsequent actions can be invoked even if we already have the response object.

This is important for things like logging the amount of time needed to handle the request.

  • In Express, one has to listen on the request/response objects to know when the request has been handled, see on-finished.

  • In our Sequence design, on can simply start the timer at the beginning of the sequence and then stop it after send or reject finished (example code).

If we want to use a higher-level framework to provide HTTP server functionality for our REST layer, then I am proposing to use Koa instead of Express. AFAIK, Koa has better support for async/await style of programming and most importantly, it's designed in such way that middleware is executed even after the response object has been constructed.

@bajtos
Copy link
Member

bajtos commented Mar 5, 2018

Few more high-level ideas and questions to consider:

1] It's already possible to mount a LB4 REST server on any Express application:

const lbApp = new RestApplication();
const expressApp = express();
expressApp.use('/api', lbApp.getSync<RestServer>('servers.RestServer').handleHttp);

If/when we land #1084, the example will become even shorter:

const lbApp = new RestApplication();
const expressApp = express();
expressApp.use('/api', lbApp.handleHttp);

2] How do you envision the interaction of our Sequence with Express/Koa middleware?

  • Do you expect express middleware to be invoked inside LB4 Sequence, e.g. as part of parseParams sequence action? If we expect express middleware to be invoked only before LB4 Sequence, then I think my solution described above may be good enough.
  • If you are expecting sequence actions to invoke express middleware, then how do you envision to handle flow control, especially in the case when the middleware does not invoke next?
  • Who is going to be responsible for handling unknown request paths (404 Not Found) and for general error handling? Should LB4 Sequence behave like a good Express middleware and invoke next for both unknown paths and errors? How are we going to capture this in our Sequence design?

@raymondfeng
Copy link
Contributor Author

raymondfeng commented Mar 5, 2018

@bajtos Thank you for the feedback. Let me clarify my view:

  1. I see Express as a provider for http/https transport so that we can get a better/richer handling than the Node core http/https modules. We can implement the http transport using Koa, Hapi, and other modules (Do not repeat yourself). Other transports can be plugged into LoopBack too. Each of our protocol servers can expose the transport level handler so that it can be mounted to the transport framework.

  2. Express middleware is the native programming model for the Express-based http transport. Developers should only use them as the last resort and should limit such usage to the transport scope/stage.

  3. Our sequence/action based post-transport processing is what most LoopBack developers should use. Some of the transport-level objects such as Request/Response will have some impact.

The req/res processing pipeline will look like:

  • Express request chain --> LoopBack sequence --> Express response chain (optional)
    or
  • Koa request/response chain --> await LoopBack sequnce

On topics of Express vs. Koa, I have debated with myself too.

  1. I like the Koa's cascading middleware design (await next()) and the usage of Context to wrap request/response/...

  2. Express definitely beats Koa in adoption, maturity, and compatibility. Think about the list of middleware and frameworks like passport.

@raymondfeng raymondfeng force-pushed the express branch 7 times, most recently from d21ecba to 3ec9a9f Compare March 6, 2018 21:16
@raymondfeng raymondfeng force-pushed the express branch 2 times, most recently from 21afc99 to d2e1197 Compare March 17, 2018 04:47
@raymondfeng raymondfeng changed the title [RFC] feat: use express as the http framework [RFC] feat: use express/koa/... as the http framework Mar 17, 2018
@bajtos
Copy link
Member

bajtos commented Mar 19, 2018

@raymondfeng I see your point.

Express request chain --> LoopBack sequence --> Express response chain (optional)

AFAIK, there is no response chain in Express and that's my concern! Re-posting my earlier question - what's you answer please?

Who is going to be responsible for handling unknown request paths (404 Not Found) and for general error handling? Should LB4 Sequence behave like a good Express middleware and invoke next for both unknown paths and errors? How are we going to capture this in our Sequence design?

Express definitely beats Koa in adoption, maturity, and compatibility. Think about the list of middleware and frameworks like passport.

I agree with you that Express beats Koa in adoption, maturity and compatibility.

OTOH, the same thing can be said about callbacks - they beat Promises and async/await in adoption and maturity too. And yet when starting LoopBack Next, we decided to throw callbacks away and use async/await everywhere.

Shouldn't we make the same bold move here, say Express good bye and focus only on integration with frameworks that are designed for async/await?

Anyhow, as long as we are able to find a solid solution for integrating our sequence-action based request processing with next-callback-based middleware style of Express, then I am find with supporting Express too.

Express middleware is the native programming model for the Express-based http transport. Developers should only use them as the last resort and should limit such usage to the transport scope/stage.
Our sequence/action based post-transport processing is what most LoopBack developers should use. Some of the transport-level objects such as Request/Response will have some impact.
Express definitely beats Koa in adoption, maturity, and compatibility. Think about the list of middleware and frameworks like passport.

I am confused. At one hand, you are saying that Express middleware should be only the last resort, but then you are mentioning Passport, which is IMO not something to add to LoopBack as the last resort solution.

Are you expecting people to use Passport with Express middleware? If so, then how do you envision integration with LoopBack4 - e.g. how to access the current user set by Passport via LB4 dependency injection? If not, then what kind of middleware do you expect people to use in LB4+Express applications?

I'd like to better understand the use cases you have in mind, so that we can implement a solution that's well tailored to those use cases.

@raymondfeng
Copy link
Contributor Author

Let me clarify. I see a high-level Node.js http framework such as Express, Koa, or Hapi adds the following features on top of Node core apis:

  1. An enhanced version of Request and Response object to simplify http request/response handling.
  2. A middleware based programming model that allows extensions to help process http requests/responses in steps
  3. A http request router (Express, not Koa)

My view to embrace one of the frameworks is:

  1. We'll get the enhanced Request and Response that a lot of Node.js developers already assumes or expects, for example, the passport framework. This will allows us to reuse existing Express middleware logic without the middleware programming model.

  2. We encapsulate the middleware chain to the transport level to handle transport/protocol specific things. Our handler will take care of LoopBack programming model, such as Sequence/Action. It might be something like: express request middleware handlers --> LoopBack 4 http handler --> express response middleware handlers.. Adding a transport layer middleware is the last resort.

  3. I expect we deal with routing for LoopBack controlled routes.

@raymondfeng
Copy link
Contributor Author

There are also a few objectives that I want to achieve:

  1. Create a HttpContext type to wrap request/response for much better flexibility and extensibility.
  2. Refactor the http layer constructs and logic out of the @loopback/rest module for better reusability and cleaner separations.
  3. Make it possible to enhance Node core IncomingMessage/ServerResponse APIs to be compatible with Express/Koa/... so that we can accommodate existing Express/Koa/... friendly modules.
  4. Do not reinvent the wheel for typical http protocol processing, such as compression, etag, cors, ...
  5. Make it easy/possible to switch from one http framework to the other if necessary
  6. Do not expose the ugly part of the underling http framework

debug('Finishing request: %s', request.originalUrl);
next();
})
.catch(err => next(err));
Copy link
Member

Choose a reason for hiding this comment

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

When next() called on L53 throws an error, this error is converted to a rejected promise, which is passed via next(err). As a result, the next callback is called twice.

Here is a better version:

handler(httpCtx).then(
  () => {
    debug('Finishing request: %s', request.originalUrl);
    next();
  },
  next);

res: response,
request,
response,
next,
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it's a good idea to expose next as a public property of HttpContext that's available to all users. At minimum, we should add some sort of a guard that will throw a helpful error if/when next is called multiple times for the same request.

implements HttpFactory<Request, Response, HttpApplication> {
createEndpoint(config: HttpServerConfig, handler: HttpHandler) {
// Create an express representing the server endpoint
const app = express() as HttpApplication;
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, I'd like the factory function express() to be configurable, so that users can customize the express instance and/or configuration. More importantly, I'd like users to be able to choose a different express-compatible server implementation like https://www.fastify.io

if (err) throw err;
});
const request = Object.setPrototypeOf(req, expressRequest);
const response = Object.setPrototypeOf(res, expressResponse);
Copy link
Member

Choose a reason for hiding this comment

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

See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf

Warning: Changing the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine. The effects on performance of altering inheritance are subtle and far-flung, and are not limited to simply the time spent in Object.setPrototypeOf(...) statement, but may extend to any code that has access to any object whose [[Prototype]] has been altered. If you care about performance you should avoid setting the [[Prototype]] of an object. Instead, create a new object with the desired [[Prototype]] using Object.create().

Can we find a different solution please, one that uses idiomatic JavaScript/TypeScript?

Also, I don't understand why is it necessary to add express prototype to the req/res objects, when createHttpContext is called from toMiddleware that already receives req/res objects from express, therefore these objects already have express additions.

requestContext.bind(RestBindings.Http.REQUEST).to(req);
requestContext.bind(RestBindings.Http.RESPONSE).to(res);
requestContext.bind(RestBindings.Http.REQUEST).to(httpCtx.request);
requestContext.bind(RestBindings.Http.RESPONSE).to(httpCtx.response);
Copy link
Member

Choose a reason for hiding this comment

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

I think we should replace request/response bindings with a single binding holding the full HTTP context. Thoughts?

@bajtos
Copy link
Member

bajtos commented Apr 26, 2018

@raymondfeng As I see it, this pull request contains multiple changes that could be made independently:

  1. A pair of (req, res) arguments was changed to a single httpContext object.
  2. Rework of REST internals to support pluggable HTTP transports (Express, Koa, etc.)
  3. Introduction of Endpoint classes that encapsulate creation of HTTP/HTTPs servers, etc.

A pair of (req, res) arguments was changed to a single httpContext object.

Could you please extract the first change, introduction of httpContext object, into a standalone pull request? A couple of important points to consider:

  • All documentation needs to be reviewed and updated to reflect the changes.
  • Using the name "context" for the object containing HTTP request/response is confusing, because we also have Context class with app-level and request-level instances. Let's find a better, less ambiguous name please!

Rework of REST internals to support pluggable HTTP transports (Express, Koa, etc.)

Is this still needed, considering the discussion in #1255? IIUC, Koa and Express are so incompatible that it's not possible (or at least too cumbersome) to support both.

In that light, can we simplify the changes you are proposing here and always assume both Request and Response objects are coming from Express, with all helper methods and parsed request metadata already attached to it?

One more thing to consider - the current implementation on master already supports mounting of LB4 applications as an Express middleware:

const loopBackApp = new RestApplication();
const expressApp = express();

express.use(loopBackApp. requestHandler);

IMO opinion, this is much simpler than what you are proposing in this pull request and fulfills most of what we need (at least for DP3). The only change that may be needed is to extend requestHandler to accept a third next argument: when the argument is provided (by Express), errors should be passed to that callback; when the argument is omitted (when "mounted" directly on core HttpServer), then the current error handling should be triggered.

Introduction of Endpoint classes that encapsulate creation of HTTP/HTTPs servers, etc.

I think this is reasonably independent of the other two changes so maybe the work on this can start in parallel with item 1 (in its own dedicated pull request)?

@hacksparrow
Copy link
Contributor

@bajtos all points noted. I will be working on this PR and the related tasks.

@bajtos
Copy link
Member

bajtos commented Jun 7, 2018

@raymondfeng I am proposing to close this pull request in favor of newer ones. Any objections?

@dhmlau
Copy link
Member

dhmlau commented Jun 20, 2018

@raymondfeng @hacksparrow , i think we've extracted some code from this PR and created another PR (and merged). Can we close this PR?

@raymondfeng
Copy link
Contributor Author

Sure

@dhmlau dhmlau closed this Jun 20, 2018
@bajtos bajtos deleted the express branch October 4, 2018 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants