Skip to content

Commit

Permalink
feat(rest): allow static assets to be served by a rest server
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Aug 31, 2018
1 parent 8ddf793 commit 11d970e
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 2 deletions.
28 changes: 27 additions & 1 deletion docs/site/Application.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,36 @@ This means you can call these `RestServer` functions to do all of your
server-level setups in the app constructor without having to explicitly retrieve
an instance of your server.

### Serve static files

The `RestServer` allows static files to be served. It can be set up by calling
the `static()` API.

```ts
app.static('/html', rootDirForHtml);
```
or
```ts
server.static(['/html', '/public'], rootDirForHtml);
```

Static assets are not allowed to be mounted on `/` to avoid performance penalty
as `/` matches all paths and incurs file system access for each HTTP request.

The static() API delegates to
[serve-static](https://expressjs.com/en/resources/middleware/serve-static.html)
to serve static files. Please see
https://expressjs.com/en/starter/static-files.html and
https://expressjs.com/en/4x/api.html#express.static for details.

**WARNING**:
> The static assets are served before LoopBack sequence of actions. If an error
is thrown, the `reject` action will NOT be triggered.

### Use unique bindings

Use binding names that are prefixed with a unique string that does not overlap
with loopback's bindings. As an example, if your application is built for your
with LoopBack's bindings. As an example, if your application is built for your
employer FooCorp, you can prefix your bindings with `fooCorp`.

```ts
Expand Down
15 changes: 15 additions & 0 deletions packages/rest/src/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
ControllerFactory,
} from './router/routing-table';
import {OperationObject, OpenApiSpec} from '@loopback/openapi-v3-types';
import {ServeStaticOptions} from 'serve-static';
import {PathParams} from 'express-serve-static-core';

export const ERR_NO_MULTI_SERVER = format(
'RestApplication does not support multiple servers!',
Expand Down Expand Up @@ -83,6 +85,19 @@ export class RestApplication extends Application implements HttpServerLike {
this.restServer.handler(handlerFn);
}

/**
* Mount static assets to the REST server.
* See https://expressjs.com/en/4x/api.html#express.static
* @param path The path(s) to serve the asset.
* See examples at https://expressjs.com/en/4x/api.html#path-examples
* To avoid performance penalty, `/` is not allowed for now.
* @param rootDir The root directory from which to serve static assets
* @param options Options for serve-static
*/
static(path: PathParams, rootDir: string, options?: ServeStaticOptions) {
this.restServer.static(path, rootDir, options);
}

/**
* Register a new Controller-based route.
*
Expand Down
37 changes: 37 additions & 0 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import {
import {RestBindings} from './keys';
import {RequestContext} from './request-context';
import * as express from 'express';
import {ServeStaticOptions} from 'serve-static';
import {PathParams} from 'express-serve-static-core';
import * as pathToRegExp from 'path-to-regexp';

const debug = require('debug')('loopback:rest:server');

Expand Down Expand Up @@ -131,6 +134,7 @@ export class RestServer extends Context implements Server, HttpServerLike {
protected _httpServer: HttpServer | undefined;

protected _expressApp: express.Application;
protected _routerForStaticAssets: express.Router;

get listening(): boolean {
return this._httpServer ? this._httpServer.listening : false;
Expand Down Expand Up @@ -196,6 +200,9 @@ export class RestServer extends Context implements Server, HttpServerLike {
};
this._expressApp.use(cors(corsOptions));

// Place the assets router here before controllers
this._setupRouterForStaticAssets();

// Mount our router & request handler
this._expressApp.use((req, res, next) => {
this._handleHttpRequest(req, res, options!).catch(next);
Expand All @@ -209,6 +216,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
*/
protected _setupRouterForStaticAssets() {
if (!this._routerForStaticAssets) {
this._routerForStaticAssets = express.Router();
this._expressApp.use(this._routerForStaticAssets);
}
}

protected _handleHttpRequest(
request: Request,
response: Response,
Expand Down Expand Up @@ -513,6 +531,25 @@ export class RestServer extends Context implements Server, HttpServerLike {
);
}

/**
* Mount static assets to the REST server.
* See https://expressjs.com/en/4x/api.html#express.static
* @param path The path(s) to serve the asset.
* See examples at https://expressjs.com/en/4x/api.html#path-examples
* To avoid performance penalty, `/` is not allowed for now.
* @param rootDir The root directory from which to serve static assets
* @param options Options for serve-static
*/
static(path: PathParams, rootDir: string, options?: ServeStaticOptions) {
const re = pathToRegExp(path, [], {end: false});
if (re.test('/')) {
throw new Error(
'Static assets cannot be mount to "/" to avoid performance penalty.',
);
}
this._routerForStaticAssets.use(path, express.static(rootDir, options));
}

/**
* Set the OpenAPI specification that defines the REST API schema for this
* server. All routes, parameter definitions and return types will be defined
Expand Down
8 changes: 8 additions & 0 deletions packages/rest/test/integration/fixtures/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<header>
<title>Test Page</title>
</header>
<body>
<h1>Hello, World!</h1>
</body>
</html>
105 changes: 104 additions & 1 deletion packages/rest/test/integration/rest.server.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {IncomingMessage, ServerResponse} from 'http';
import * as yaml from 'js-yaml';
import * as path from 'path';
import * as fs from 'fs';
import {RestServerConfig} from '../../src';

describe('RestServer (integration)', () => {
it('exports url property', async () => {
Expand Down Expand Up @@ -77,6 +78,108 @@ describe('RestServer (integration)', () => {
.expect(500);
});

it('does not allow static assets to be mounted at /', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
},
});

expect(() => server.static('/', root)).to.throw(
'Static assets cannot be mount to "/" to avoid performance penalty.',
);

expect(() => server.static('', root)).to.throw(
'Static assets cannot be mount to "/" to avoid performance penalty.',
);

expect(() => server.static(['/'], root)).to.throw(
'Static assets cannot be mount to "/" to avoid performance penalty.',
);

expect(() => server.static(['/html', ''], root)).to.throw(
'Static assets cannot be mount to "/" to avoid performance penalty.',
);

expect(() => server.static(/.*/, root)).to.throw(
'Static assets cannot be mount to "/" to avoid performance penalty.',
);

expect(() => server.static('/(.*)', root)).to.throw(
'Static assets cannot be mount to "/" to avoid performance penalty.',
);
});

it('allows static assets via api', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
},
});

server.static('/html', root);
const content = fs
.readFileSync(path.join(root, 'index.html'))
.toString('utf-8');
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect('Content-Type', /text\/html/)
.expect(200, content);
});

it('allows static assets via api after start', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
},
});
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect(404);

server.static('/html', root);

await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect(200);
});

it('allows non-static routes after assets', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
},
});
server.static('/html', root);
server.handler(dummyRequestHandler);

await createClientForHandler(server.requestHandler)
.get('/html/does-not-exist.html')
.expect(200, 'Hello');
});

it('serve static assets if matches before other routes', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
},
});
server.static('/html', root);
server.handler(dummyRequestHandler);

const content = fs
.readFileSync(path.join(root, 'index.html'))
.toString('utf-8');
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect(200, content);
});

it('allows cors', async () => {
const server = await givenAServer({rest: {port: 0}});
server.handler(dummyRequestHandler);
Expand Down Expand Up @@ -369,7 +472,7 @@ servers:
await server.stop();
});

async function givenAServer(options?: ApplicationConfig) {
async function givenAServer(options?: {rest: RestServerConfig}) {
const app = new Application(options);
app.component(RestComponent);
return await app.getServer(RestServer);
Expand Down

0 comments on commit 11d970e

Please sign in to comment.