-
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): allow static assets to be served by a rest server #1611
Conversation
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.
YAY!! 💯 🥇 👍 🙇
Much needed feature IMO! I also love how simple the implementation is.
Just needs some documentation -- in the package README + loopback.io docs and this is good to land. Maybe a quick blog, if not we can include it in the monthly milestone blog.
Related to #691 Based on the issue, what are your thoughts on introducing a sugar method |
3fab97d
to
b0c4f80
Compare
@virkt25 Added more tests and an |
b0c4f80
to
e04f2da
Compare
I don't see |
@rmg |
Shouldn't it still be listed as a dependency then? Otherwise you're relying on dependency flattening for this to work. |
@rmg We only use |
packages/rest/src/rest.server.ts
Outdated
* @param options Options for serve-static | ||
*/ | ||
asset(path: string, root: string, options?: ServeStaticOptions) { | ||
this._setupAssetsRouter(); |
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.
Should we use the same static router for all static files because right now each folder would get it's own.
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.
No, we have the same router - this._assetsRouter
. It's the container for multiple middleware, each per root dir.
* @param root The root directory | ||
* @param options Options for serve-static | ||
*/ | ||
asset(path: string, root: string, options?: ServeStaticOptions) { |
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 rename this to static
-- it's traditional and what developers would expect. Asset to me can imply anything ... a controller can be an asset imo.
Oh, I see how it is being used now.. I didn't notice the middleware was used as |
e04f2da
to
b80287b
Compare
@virkt25 Made some name changes. |
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. One last comment + needs docs (if that’ll be a follow up PR then just the one last comment).
@@ -196,6 +198,21 @@ export class RestServer extends Context implements Server, HttpServerLike { | |||
}; | |||
this._expressApp.use(cors(corsOptions)); | |||
|
|||
// Place the assets router here before controllers | |||
this._setupRouterForStaticAssets(); |
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.
This can be moved to be inside the if statement below so we only instantiate this router if a static asset is being added.
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.
No, I have it outside of the if
statement on purpose as we need to register the middleware slot so that .static()
will work.
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.
But you are calling this function from the .static()
method as well ... so maybe remove it from there?
b80287b
to
736071d
Compare
@virkt25 Added some docs. |
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 once the last comment is addressed.
packages/rest/src/rest.server.ts
Outdated
* @param options Options for serve-static | ||
*/ | ||
static(path: string, root: string, options?: ServeStaticOptions) { | ||
this._setupRouterForStaticAssets(); |
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.
This isn’t needed because of the line above.
b54eccc
to
038d99c
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.
Serving static files is tricky, let's make sure we get it right.
Besides the comments below, please add a test to verify that new static assets can be registered after the app was started.
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect(404);
server.static('/html', /*...*/);
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect(200);
@@ -196,6 +198,21 @@ export class RestServer extends Context implements Server, HttpServerLike { | |||
}; | |||
this._expressApp.use(cors(corsOptions)); | |||
|
|||
// Place the assets router here before controllers |
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.
❗️ The static router must be mounted AFTER dynamic routes. This is super important for performance. ❗️
In your current design, every REST API endpoint like /products/123
is going to hit the file system to check if there is a file matching the URL, e.g. /html/products/123
.
Please add a test to verify what happens when there is both a LB4 route (e.g. a controller method) and a static file mapped at the same URL.
The correct order of middleware:
- LB4 router
- static files
- error handler
This is BTW the same design we have in LB 3.x, it took us few iterations and lot of learning until we figured it out correctly.
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.
In your current design, every REST API endpoint like /products/123 is going to hit the file system to check if there is a file matching the URL, e.g. /html/products/123.
That's not true. /products/123
won't match /html
and it will be skipped without touching the file system.
The correct order of middleware:
LB4 router
static files
error handler
I'm fine with this order but the LB4 router does not pass control to next
at the moment.
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 thought express makes a map of the routes in memory of all the files mounted for static serving? Isn't that the point of the express Router?
@@ -209,6 +226,17 @@ export class RestServer extends Context implements Server, HttpServerLike { | |||
); | |||
} | |||
|
|||
/** | |||
* Set up an express router for all static assets so that middleware for | |||
* all directories are invoked at the same phase |
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.
Please note that each new Router layer adds a non-negligible delay to the observed latency (time needed to handle the request) and also reduces the amount of requests that the server can handle every second.
I don't understand what's the purpose of creating a new express.Router()
dedicated to serving static assets? Is this needed to support app.static
API? If it is, then I am proposing to use a different implementation where we collect all information provided by app.static
(e.g. list of arguments passed to every invocation) and then invoke static middleware factory only when constructing the ultimate request handler.
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 think this should be mostly an implementation detail that we can easily change later. If you prefer, then I am ok to keep the currently proposed implementation for now.
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's the need to support app.static()
api. This is the same express problem that we worked around in LB3 with middleware phases.
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 understand that and wanted to proposed a different solution to that problem. Let's not waste too much time on this, I can later send a follow-up pull request to update the implementation to what I am envisioning. However, based on my other comment, we may need the extra express.Router
instance to allow sequence-based implementation, in which case this discussion is not relevant.
Let's move the discussion to the main thread so that it does not get hidden when you change the code.
Ah, you are right. I agree that when the static middleware is mounted at a path that's different from API endpoints, then the order of the LB4 router vs. serve-static does not matter. Should we look for a way how to ensure that static middleware is always mounted on a non-root path that prevents conflicts with route/controller-based paths? My concern is that people will use this feature to build single-page applications and mount the static middleware at the root, to be able to serve
Hmm, this is actually more tricky than it looks at the first sight. In LB4, API requests are handled by a Sequence, which is designed with the assumption that the Sequence is going to handle all requests and implement error handling too (including 404 not found). IMO, removing this assumption and changing the design of Sequence to add a third possible outcome - let somebody else handle the request - would be a big step backwards and we should avoid that. Also mounting serve-static middleware at Express layer is bypassing the sequence entirely. This is likely to have unintended consequences that may bite us hard. For example, happens serve-static cannot read a file, e.g. because of a permission problem, then it forward the error via I am wondering: Can we define a special kind of a catch-all route that will:
In my mind, such solution would solve most if not all issues:
It does not. For example, in the development mode, you want the serve-static middleware to pick up any new files you added to your application (e.g. new front-end assets) without having to restart the applicaton. Another example - a file storage service allowing clients to upload new assets that are later publicly available for download. I agree it would be great if there was an option how to scan existing files and build an in-memory map of static-file routes, unfortunately serve-static does not support this mode. |
I think a For both function staticMiddleware(ctx, staticFileMap) {
let staticRouter = ctx.getSync('express.routers.static', {optional: true});
if (staticRouter === undefined) {
staticRouter = express.Router();
// Mount initial set of static files to the router
for(const p in staticFileMap) {
staticRouter.use(p, ...);
}
ctx.bind('express.routers.static').to(staticRouter);
}
return (req, res, next) => {
// const staticRouter = ctx.getSync('express.routers.static');
staticRouter(req, res, next);
}
} The static router maintains a map of (path, root) for static files and it can be dynamically updated without impacting the express middleware chain. |
038d99c
to
e41579d
Compare
BTW, I think we should refine the solution iteratively. For now, let's make it possible to serve static files. In the longer term, we should come up a better design that allow Express middleware and LB4 Sequence of actions to be friendly to each other. |
cd9a78c
to
ae72ebc
Compare
Sure. I have two main concerns about the current proposal.
Please add the following tests:
|
I see this is already covered by tests 👍 please ignore this part of my previous comment. |
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.
Few more comments in addition to what I wrote above (#1611 (comment)).
* @param root The root directory | ||
* @param options Options for serve-static | ||
*/ | ||
static(path: string, root: string, options?: ServeStaticOptions) { |
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 find the names path
and root
too similar; it's too easy to confuse which one is for the URL path and which one is for the filesystem path. I am proposing the following renames:
path
->url
,urlPath
orendpointPath
root
->rootDir
,fileRoot
orserveFrom
Please update RestServer#static
in the same way too.
packages/rest/src/rest.server.ts
Outdated
@@ -670,6 +708,15 @@ export interface RestServerOptions { | |||
cors?: cors.CorsOptions; | |||
apiExplorerUrl?: string; | |||
sequence?: Constructor<SequenceHandler>; | |||
/** | |||
* Static assets to be served using `express.static()` |
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 #742.
The ApplicationConfig class should contain only things like HTTP port and database connection strings, there should be no entries related to what component, controller, repositories (etc.) the application is using. User application classes should call this.controller(), this.repository() (etc.) to register their artifacts, and eventually use the new bootstrapper module.
Please remove this config option in favor of app.static
API.
ae72ebc
to
c9317a0
Compare
@bajtos Most of your comments have been addressed. 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.
Awesome, almost there.
Besides the comment below, could you please add the following test?
- What happens when there is both a LB4 route (e.g. a controller method) and a static file mapped at the same URL.
Unless you want to keep this behavior as undefined to allow us to change it within a semver-minor version, in which case I'd like to see this undefined behavior clearly spelled out in docs/site/Application.md
.
If you can get somebody else from the team to approve this pull request after you address my two comments, then feel free to proceed with landing without waiting for another review from me.
packages/rest/src/rest.server.ts
Outdated
path === '/' || // path is '/' | ||
// '' or '/' is included in the array | ||
(Array.isArray(path) && (path.includes('/') || path.includes(''))) | ||
) { |
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.
Please note that path
can be also a regular expression.
I am proposing to reject all Regexp-based path values. In the future, I'd like to handle as much routing as possible using a fast trie-based router and trie-based routing does not support arbitrary regexp routes. If we allow regexp routes now, then the change to a trie-based router would be backwards incompatible and would have to wait for a next semver-major release.
If you agree to reject regexp values, then please update tsdoc comments and the content in docs/site/Application.md
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.
Please note that string paths can allow regex patterns. I'll use path-to-regexp
to check if path
matches /
.
c9317a0
to
dc80a46
Compare
@bajtos I improved the code and tests per your comment. |
dc80a46
to
11d970e
Compare
11d970e
to
2fb8a59
Compare
Follow-up stories to address missing pieces:
|
This PR allows static assets to be served from a REST server by calling
static()
.Missing tests identified by @bajtos
When a file cannot be read (e.g. because of permission problems), the error response is sent by the Sequence (as provided/customized by the app).Checklist
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated