diff --git a/packages/http-server/README.md b/packages/http-server/README.md index 2587dcdb0b93..e25ddb44ab10 100644 --- a/packages/http-server/README.md +++ b/packages/http-server/README.md @@ -4,46 +4,40 @@ This package implements the HTTP / HTTPS server endpoint for LoopBack 4 apps. ## Overview -This is an internal package used by `RestServer` for creating its HTTP / HTTPS server. +This is an internal package used by LoopBack 4 for creating HTTP / HTTPS server. ## Installation To use this package, you'll need to install `@loopback/http-server`. ```sh -npm i @loopback/htp-server +npm i @loopback/http-server ``` ## Usage -`@loopback/http-server` should be instantiated with an instance of `RestServer`, HTTP / HTTPS options object, and a request handler function. +`@loopback/http-server` should be instantiated with a request handler function, and an HTTP / HTTPS options object. ```js -import {RestServer} from '@loopback/rest'; -import {Application} from '@loopback/core'; - -const app = new Application(); -const restServer = new RestServer(app); -const httpServer = new HttpServer(restServer, {port: 3000, host: ''}, (req, res) => {}); +const httpServer = new HttpServer((req, res) => { res.end('Hello world')}, {port: 3000, host: ''}); ``` -Call the `start()` method to start the server. - -```js -httpServer.start() -``` +Instance methods of `HttpServer`. -Call the `stop()` method to stop the server. +| Method | Description | +| ------- | -------------------- | +| `start()` | Starts the server | +| `stop()` | Stops the server | -Use the `listening` property to check whether the server is listening for connections or not. +Instance properties of `HttpServer`. -```js -if (httpServer.listening) { - console.log('Server is running'); -} else { - console.log('Server is not running'); -} -``` +| Property | Description | +| ----------- | ---------------------- | +| `address` | Address details | +| `host` | host of the server | +| `port` | port of the server | +| `protocol` | protocol of the server | +| `url` | url the server | ## Contributions diff --git a/packages/http-server/package.json b/packages/http-server/package.json index 0246bf13f249..8def85b8b2fc 100644 --- a/packages/http-server/package.json +++ b/packages/http-server/package.json @@ -1,6 +1,6 @@ { "name": "@loopback/http-server", - "version": "0.0.0", + "version": "0.0.1", "description": "", "engines": { "node": ">=8" @@ -12,35 +12,35 @@ "build:current": "lb-tsc", "build:dist8": "lb-tsc es2017", "build:dist10": "lb-tsc es2018", - "clean": "lb-clean loopback-core*.tgz dist* package api-docs", + "clean": "lb-clean loopback-http-server*.tgz dist* package api-docs", "pretest": "npm run build:current", "integration": "lb-mocha \"DIST/test/integration/**/*.js\"", "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/integration/**/*.js\" \"DIST/test/acceptance/**/*.js\"", "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", - "verify": "npm pack && tar xf loopback-core*.tgz && tree package && npm run clean" + "verify": "npm pack && tar xf loopback-http-server*.tgz && tree package && npm run clean" }, "author": "IBM", "copyright.owner": "IBM Corp.", "license": "MIT", "dependencies": { "@loopback/dist-util": "^0.3.1", - "@loopback/rest": "^0.10.4", "p-event": "^2.0.0" }, "devDependencies": { "@loopback/build": "^0.6.5", "@loopback/core": "^0.8.4", "@loopback/testlab": "^0.10.4", - "@types/debug": "^0.0.30", "@types/node": "^10.1.2", - "@types/p-event": "^1.3.0" + "@types/p-event": "^1.3.0", + "@types/request-promise-native": "^1.0.14", + "request-promise-native": "^1.0.5" }, "files": [ "README.md", "index.js", "index.d.ts", - "dist/src", - "api-docs", + "dist*/src", + "dist*/index*", "src" ], "repository": { diff --git a/packages/http-server/src/http-server.ts b/packages/http-server/src/http-server.ts index 99402983c9e5..738dac3a7167 100644 --- a/packages/http-server/src/http-server.ts +++ b/packages/http-server/src/http-server.ts @@ -3,19 +3,26 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {createServer, Server} from 'http'; -import {RestServer, HttpRequestListener} from '@loopback/rest'; +import {createServer, Server, ServerRequest, ServerResponse} from 'http'; import {AddressInfo} from 'net'; import * as pEvent from 'p-event'; +export type HttpRequestListener = ( + req: ServerRequest, + res: ServerResponse, +) => void; + /** * Object for specifyig the HTTP / HTTPS server options */ -export type HttpOptions = { - port: number; - host: string | undefined; +export type HttpServerOptions = { + port?: number; + host?: string; + protocol?: HttpProtocol; }; +export type HttpProtocol = 'http' | 'https'; // Will be extended to `http2` in the future + /** * HTTP / HTTPS server used by LoopBack's RestServer * @@ -23,63 +30,85 @@ export type HttpOptions = { * @class HttpServer */ export class HttpServer { - private restServer: RestServer; - private httpPort: number; - private httpHost: string | undefined; + private _port: number; + private _host?: string; + /** + * Protocol, default to `http` + */ + private _protocol: HttpProtocol; + private _address: AddressInfo; + private httpRequestListener: HttpRequestListener; private httpServer: Server; /** - * @param restServer - * @param httpOptions + * @param httpServerOptions * @param httpRequestListener */ constructor( - restServer: RestServer, - httpOptions: HttpOptions, httpRequestListener: HttpRequestListener, + httpServerOptions?: HttpServerOptions, ) { - this.restServer = restServer; - this.httpPort = httpOptions.port; - this.httpHost = httpOptions.host; - this.httpServer = createServer(httpRequestListener); + this._port = (httpServerOptions && httpServerOptions.port) || 0; + this._host = (httpServerOptions && httpServerOptions.host) || undefined; + this._protocol = + (httpServerOptions && httpServerOptions.protocol) || 'http'; + this.httpRequestListener = httpRequestListener; } /** * Starts the HTTP / HTTPS server */ - public start(): Promise { - this.httpServer.listen(this.httpPort, this.httpHost); - return new Promise(async (resolve, reject) => { - try { - await pEvent(this.httpServer, 'listening'); - const address = this.httpServer.address() as AddressInfo; - this.restServer.bind('rest.port').to(address.port); - resolve(); - } catch (e) { - reject(); - } - }); + public async start(port?: number, host?: string) { + this.httpServer = createServer(this.httpRequestListener); + this._port = typeof port === 'undefined' ? 0 : this._port; + this._host = host || this._host; + this.httpServer.listen(this._port, this._host); + await pEvent(this.httpServer, 'listening'); + this._address = this.httpServer.address() as AddressInfo; + this._host = this._host || this._address.address; + this._port = this._address.port; } /** * Stops the HTTP / HTTPS server */ - public stop(): Promise { + public async stop() { this.httpServer.close(); - return new Promise(async (resolve, reject) => { - try { - await pEvent(this.httpServer, 'close'); - resolve(); - } catch (e) { - reject(); - } - }); + await pEvent(this.httpServer, 'close'); + } + + /** + * Protocol of the HTTP / HTTPS server + */ + public get protocol(): HttpProtocol { + return this._protocol; + } + + /** + * Port number of the HTTP / HTTPS server + */ + public get port(): number { + return this._port; + } + + /** + * Host of the HTTP / HTTPS server + */ + public get host(): string | undefined { + return this._host; + } + + /** + * URL of the HTTP / HTTPS server + */ + public get url(): string { + return `${this._protocol}://${this.host}:${this.port}`; } /** - * Whether the HTTP / HTTPS server is listening or not + * Address of the HTTP / HTTPS server */ - public get listening(): Boolean { - return this.httpServer.listening; + public get address(): AddressInfo { + return this._address; } } diff --git a/packages/http-server/test/integration/http-server.integration.ts b/packages/http-server/test/integration/http-server.integration.ts index 50b23e8e8954..c1c89da54128 100644 --- a/packages/http-server/test/integration/http-server.integration.ts +++ b/packages/http-server/test/integration/http-server.integration.ts @@ -2,28 +2,81 @@ // Node module: @loopback/http-server // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {RestServer, RestComponent} from '@loopback/rest'; -import {Application, ApplicationConfig} from '@loopback/core'; -import * as assert from 'assert'; +import {HttpServer} from '../../'; +import {supertest, expect} from '@loopback/testlab'; +import * as makeRequest from 'request-promise-native'; +import {ServerRequest, ServerResponse} from 'http'; -describe('HttpServer', () => { +describe('HttpServer (integration)', () => { it('starts server', async () => { - const server = await givenAServer(); + const server = new HttpServer(dummyRequestHandler); await server.start(); - assert(server.listening, 'Server not started'); + supertest(server.url) + .get('/') + .expect(200); await server.stop(); }); it('stops server', async () => { - const server = await givenAServer(); + const server = new HttpServer(dummyRequestHandler); await server.start(); await server.stop(); - assert(!server.listening, 'Server not stopped'); + await expect( + makeRequest({ + uri: server.url, + }), + ).to.be.rejectedWith(/ECONNREFUSED/); }); - async function givenAServer(options?: ApplicationConfig) { - const app = new Application(options); - app.component(RestComponent); - return await app.getServer(RestServer); + it('exports port', async () => { + const server = new HttpServer(dummyRequestHandler); + await server.start(); + expect(server) + .to.have.property('port') + .which.is.a.Number() + .which.is.greaterThan(0); + await server.stop(); + }); + + it('exports host', async () => { + const server = new HttpServer(dummyRequestHandler); + await server.start(); + expect(server) + .to.have.property('host') + .which.is.a.String(); + await server.stop(); + }); + + it('exports protocol', async () => { + const server = new HttpServer(dummyRequestHandler); + await server.start(); + expect(server) + .to.have.property('protocol') + .which.is.a.String() + .match(/http|https/); + await server.stop(); + }); + + it('exports url', async () => { + const server = new HttpServer(dummyRequestHandler); + await server.start(); + expect(server) + .to.have.property('url') + .which.is.a.String() + .match(/http|https\:\/\//); + await server.stop(); + }); + + it('exports address', async () => { + const server = new HttpServer(dummyRequestHandler); + await server.start(); + expect(server) + .to.have.property('address') + .which.is.an.Object(); + await server.stop(); + }); + + function dummyRequestHandler(req: ServerRequest, res: ServerResponse): void { + res.end(); } }); diff --git a/packages/http-server/test/unit/http-server.unit.ts b/packages/http-server/test/unit/http-server.unit.ts deleted file mode 100644 index 3d40ca39baa0..000000000000 --- a/packages/http-server/test/unit/http-server.unit.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright IBM Corp. 2018. All Rights Reserved. -// Node module: @loopback/http-server -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT -import {HttpServer} from '../..'; -import {RestServer} from '@loopback/rest'; -import {Application} from '@loopback/core'; -import * as assert from 'assert'; - -describe('HttpServer', () => { - it('starts server', async () => { - const server = await givenAServer(); - await server.start(); - assert(server.listening, 'Server not started'); - await server.stop(); - }); - - it('stops server', async () => { - const server = await givenAServer(); - await server.start(); - await server.stop(); - assert(!server.listening, 'Server not stopped'); - }); - - async function givenAServer() { - const app = new Application(); - const restServer = new RestServer(app); - return new HttpServer(restServer, {port: 3000, host: ''}, (req, res) => {}); - } -}); diff --git a/packages/rest/package.json b/packages/rest/package.json index e5801ac8c853..0f70955fa46d 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -26,7 +26,7 @@ "@loopback/context": "^0.11.2", "@loopback/core": "^0.8.4", "@loopback/openapi-v3": "^0.10.5", - "@loopback/http-server": "^0.0.0", + "@loopback/http-server": "^0.0.1", "@loopback/openapi-v3-types": "^0.7.4", "@types/cors": "^2.8.3", "@types/express": "^4.11.1", diff --git a/packages/rest/src/keys.ts b/packages/rest/src/keys.ts index 8ebc0c8d7033..3d6159eb987b 100644 --- a/packages/rest/src/keys.ts +++ b/packages/rest/src/keys.ts @@ -42,6 +42,14 @@ export namespace RestBindings { * Binding key for setting and injecting the port number of RestServer */ export const PORT = BindingKey.create('rest.port'); + /** + * Binding key for setting and injecting the URL of RestServer + */ + export const URL = BindingKey.create('rest.url'); + /** + * Binding key for setting and injecting the protocol of RestServer + */ + export const PROTOCOL = BindingKey.create<'http' | 'https'>('rest.protocol'); /** * Internal binding key for http-handler */ diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 3ddfd93903a8..48666af11b4e 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -158,6 +158,7 @@ export class RestServer extends Context implements Server, HttpServerLike { } this.bind(RestBindings.PORT).to(options.port); this.bind(RestBindings.HOST).to(options.host); + this.bind(RestBindings.PROTOCOL).to(options.protocol || 'http'); if (options.sequence) { this.sequence(options.sequence); @@ -166,15 +167,6 @@ export class RestServer extends Context implements Server, HttpServerLike { this._setupRequestHandler(options); this.bind(RestBindings.HANDLER).toDynamicValue(() => this.httpHandler); - - this._httpServer = new HttpServer( - this, - { - port: options.port, - host: options.host, - }, - this.requestHandler, - ); } protected _setupRequestHandler(options: RestServerConfig) { @@ -580,7 +572,23 @@ export class RestServer extends Context implements Server, HttpServerLike { // Setup the HTTP handler so that we can verify the configuration // of API spec, controllers and routes at startup time. this._setupHandlerIfNeeded(); - return this._httpServer.start(); + + const port = await this.get(RestBindings.PORT); + const host = await this.get(RestBindings.HOST); + const protocol = await this.get<'http' | 'https' | undefined>( + RestBindings.PROTOCOL, + ); + + this._httpServer = new HttpServer(this.requestHandler, { + port: port, + host: host, + protocol: protocol || 'http', + }); + + await this._httpServer.start(port, host); + this.bind(RestBindings.PORT).to(this._httpServer.port); + this.bind(RestBindings.HOST).to(this._httpServer.host); + this.bind(RestBindings.URL).to(this._httpServer.url); } /** @@ -591,17 +599,7 @@ export class RestServer extends Context implements Server, HttpServerLike { */ async stop() { // Kill the server instance. - return this._httpServer.stop(); - } - - /** - * Whether the REST server is listening or not - * - * @returns {Boolean} - * @memberof RestServer - */ - public get listening(): Boolean { - return this._httpServer.listening; + await this._httpServer.stop(); } protected _onUnhandledError(req: Request, res: Response, err: Error) { @@ -631,4 +629,5 @@ export interface RestServerConfig { cors?: cors.CorsOptions; apiExplorerUrl?: string; sequence?: Constructor; + protocol?: 'http' | 'https'; } diff --git a/packages/rest/test/integration/rest.server.integration.ts b/packages/rest/test/integration/rest.server.integration.ts index de5029d8f295..e2b7443f88ea 100644 --- a/packages/rest/test/integration/rest.server.integration.ts +++ b/packages/rest/test/integration/rest.server.integration.ts @@ -16,6 +16,14 @@ describe('RestServer (integration)', () => { await server.stop(); }); + it('honors port binding after instantiation', async () => { + const server = await givenAServer({rest: {port: 80}}); + await server.bind(RestBindings.PORT).to(0); + await server.start(); + expect(server.getSync(RestBindings.PORT)).to.not.equal(80); + await server.stop(); + }); + it('responds with 500 when Sequence fails with unhandled error', async () => { const server = await givenAServer({rest: {port: 0}}); server.handler((context, sequence) => {