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

feat(http-server): make http2 server creation extensible #2078

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/http-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"@types/node": "^10.11.2",
"@types/p-event": "^1.3.0",
"@types/request-promise-native": "^1.0.15",
"request-promise-native": "^1.0.5"
"@types/spdy": "^3.4.4",
"request-promise-native": "^1.0.5",
"spdy": "^4.0.0"
},
"files": [
"README.md",
Expand Down
170 changes: 90 additions & 80 deletions packages/http-server/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,129 +3,104 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {IncomingMessage, ServerResponse} from 'http';
import * as http from 'http';
import * as https from 'https';
import {AddressInfo} from 'net';

import * as pEvent from 'p-event';

export type RequestListener = (
req: IncomingMessage,
res: ServerResponse,
) => void;

/**
* Basic HTTP server listener options
*
* @export
* @interface ListenerOptions
*/
export interface ListenerOptions {
host?: string;
port?: number;
}

/**
* HTTP server options
*
* @export
* @interface HttpOptions
*/
export interface HttpOptions extends ListenerOptions {
protocol?: 'http';
}

/**
* HTTPS server options
*
* @export
* @interface HttpsOptions
*/
export interface HttpsOptions extends ListenerOptions, https.ServerOptions {
protocol: 'https';
}

/**
* Possible server options
*
* @export
* @type HttpServerOptions
*/
export type HttpServerOptions = HttpOptions | HttpsOptions;

/**
* Supported protocols
*
* @export
* @type HttpProtocol
*/
export type HttpProtocol = 'http' | 'https'; // Will be extended to `http2` in the future
import {
HttpProtocol,
HttpServer,
HttpServerOptions,
RequestListener,
ProtocolServerFactory,
} from './types';

/**
* HTTP / HTTPS server used by LoopBack's RestServer
*
* @export
* @class HttpServer
*/
export class HttpServer {
export class DefaultHttpServer implements HttpServer {
Copy link
Member

Choose a reason for hiding this comment

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

What's the impact of this change on possible users of @loopback/http-server?

For example, the following code creates a fully working HttpServer class in the current version (before your changes):

import * as turbo from 'turbo-http';

class TurboHttpServer extends HttpServer {
  constructor(
    requestListener: RequestListener,
    serverOptions?: HttpServerOptions,
  ) {
    super(requestListener, serverOptions);
    this.server = turbo.createServer(requestListener);
  }
}

IMO, we should preserve backwards compatibility in our API. Otherwise a new major version must be published, plus we will have to keep maintaining the old version according to our LTS policy...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can release @loopback/[email protected] if we have to break the compatibility due to the nature of this work.

Copy link
Member

Choose a reason for hiding this comment

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

We can release @loopback/[email protected] if we have to break the compatibility due to the nature of this work.

Fair enough. In that case:

Copy link
Member

Choose a reason for hiding this comment

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

See also #2103

private _port: number;
private _host?: string;
private _listening: boolean = false;
private _protocol: HttpProtocol;
protected _protocol: string;
private _urlScheme: string;
private _address: AddressInfo;
private requestListener: RequestListener;
readonly server: http.Server | https.Server;
private serverOptions?: HttpServerOptions;
protected readonly requestListener: RequestListener;
protected _server: http.Server | https.Server;
protected readonly serverOptions: HttpServerOptions;

protected protocolServerFactories: ProtocolServerFactory[];

/**
* @param requestListener
* @param serverOptions
*/
constructor(
requestListener: RequestListener,
serverOptions?: HttpServerOptions,
serverOptions: HttpServerOptions = {},
protocolServerFactories?: ProtocolServerFactory[],
) {
this.requestListener = requestListener;
serverOptions = serverOptions || {};
this.serverOptions = serverOptions;
this._port = serverOptions ? serverOptions.port || 0 : 0;
this._host = serverOptions ? serverOptions.host : undefined;
this._protocol = serverOptions ? serverOptions.protocol || 'http' : 'http';
if (this._protocol === 'https') {
this.server = https.createServer(
this.serverOptions as https.ServerOptions,
this.requestListener,
);
} else {
this.server = http.createServer(this.requestListener);
this._port = serverOptions.port || 0;
this._host = serverOptions.host || undefined;
this._protocol = serverOptions.protocol || 'http';
this.protocolServerFactories = protocolServerFactories || [
new HttpProtocolServerFactory(),
];
const server = this.createServer();
this._server = server.server;
this._urlScheme = server.urlScheme;
}

/**
* Create a server for the given protocol
*/
protected createServer() {
for (const factory of this.protocolServerFactories) {
if (factory.supports(this._protocol, this.serverOptions)) {
const server = factory.createServer(
this._protocol,
this.requestListener,
this.serverOptions,
);
if (server) {
return server;
}
}
}
throw new Error(`The ${this._protocol} protocol is not supported`);
}

/**
* Starts the HTTP / HTTPS server
*/
public async start() {
this.server.listen(this._port, this._host);
await pEvent(this.server, 'listening');
this._server.listen(this._port, this._host);
await pEvent(this._server, 'listening');
this._listening = true;
this._address = this.server.address() as AddressInfo;
this._address = this._server.address() as AddressInfo;
}

/**
* Stops the HTTP / HTTPS server
*/
public async stop() {
if (!this.server) return;
this.server.close();
await pEvent(this.server, 'close');
if (!this._server) return;
this._server.close();
await pEvent(this._server, 'close');
this._listening = false;
}

/**
* Protocol of the HTTP / HTTPS server
*/
public get protocol(): HttpProtocol {
return this._protocol;
public get protocol(): string {
return this._urlScheme || this._protocol;
}

/**
Expand All @@ -147,13 +122,13 @@ export class HttpServer {
*/
public get url(): string {
let host = this.host;
if (this._address.family === 'IPv6') {
if (this._address && this._address.family === 'IPv6') {
if (host === '::') host = '::1';
host = `[${host}]`;
} else if (host === '0.0.0.0') {
host = '127.0.0.1';
}
return `${this._protocol}://${host}:${this.port}`;
return `${this.protocol}://${host}:${this.port}`;
}

/**
Expand All @@ -163,10 +138,45 @@ export class HttpServer {
return this._listening;
}

public get server(): http.Server | https.Server {
return this._server;
}

/**
* Address of the HTTP / HTTPS server
*/
public get address(): AddressInfo | undefined {
return this._listening ? this._address : undefined;
}
}

export class HttpProtocolServerFactory implements ProtocolServerFactory {
/**
* Supports http and https
* @param protocol
*/
supports(protocol: string) {
return protocol === 'http' || protocol === 'https';
}

/**
* Create a server for the given protocol
*/
createServer(
protocol: string,
requestListener: RequestListener,
serverOptions: HttpServerOptions,
) {
if (protocol === 'https') {
const server = https.createServer(
serverOptions as https.ServerOptions,
requestListener,
);
return {server, urlScheme: protocol};
} else if (protocol === 'http') {
const server = http.createServer(requestListener);
return {server, urlScheme: protocol};
}
throw new Error(`The ${protocol} protocol is not supported`);
}
}
6 changes: 6 additions & 0 deletions packages/http-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
// Copyright IBM Corp. 2017,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

export * from './types';
export * from './http-server';
110 changes: 110 additions & 0 deletions packages/http-server/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {AddressInfo} from 'net';
import * as http from 'http';
import * as https from 'https';
import {IncomingMessage, ServerResponse} from 'http';

// 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

export type RequestListener = (
req: IncomingMessage,
res: ServerResponse,
) => void;

/**
* Basic HTTP server listener options
*
* @export
* @interface ListenerOptions
*/
export interface ListenerOptions {
host?: string;
port?: number;
}

/**
* HTTP server options
*
* @export
* @interface HttpOptions
*/
export interface HttpOptions extends ListenerOptions {
protocol?: 'http';
}

/**
* HTTPS server options
*
* @export
* @interface HttpsOptions
*/
export interface HttpsOptions extends ListenerOptions, https.ServerOptions {
protocol: 'https';
}

/**
* HTTP/2 server options
*
* @export
* @interface Http2Options
*/
export interface Http2Options extends ListenerOptions {
protocol: 'http2';
Copy link
Member

Choose a reason for hiding this comment

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

The built-in http2 module support HTTP/2 protocol over plain unencrypted socket (similar to HTTP) or over TLS socket (similar to HTTPS). How do you envision encoding this configuration flag when there is a single protocol value http2?

// Other options for a module like https://github.com/spdy-http2/node-spdy
[name: string]: unknown;
}

/**
* Possible server options
*
* @export
* @type HttpServerOptions
*/
export type HttpServerOptions = HttpOptions | HttpsOptions | Http2Options;

/**
* Supported protocols
*
* @export
* @type HttpProtocol
*/
export type HttpProtocol = 'http' | 'https' | 'http2';

/**
* HTTP / HTTPS server used by LoopBack's RestServer
*
* @export
* @class HttpServer
*/
export interface HttpServer {
readonly server: http.Server | https.Server;
readonly protocol: string;
readonly port: number;
readonly host: string | undefined;
readonly url: string;
readonly listening: boolean;
readonly address: AddressInfo | undefined;

start(): Promise<void>;
stop(): Promise<void>;
}

export interface ProtocolServer {
server: http.Server | https.Server;
/**
* Scheme for the URL
*/
urlScheme: string;
}

export interface ProtocolServerFactory {
supports(protocol: string, serverOptions: HttpServerOptions): boolean;
Copy link
Member

Choose a reason for hiding this comment

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

Interesting. The good part about this design is that it follows what we have already in place for body parsers. However, I feel it's unnecessarily complicated for the case of http servers.

For long term, I think we need to provide http2 support based on Node.js core module http2. From what I read, spdy does not work on Node.js 10+. Once this is done, most users should not need to change the way how HTTP servers are created at all.


createServer(
protocol: string,
requestListener: RequestListener,
serverOptions: HttpServerOptions,
): ProtocolServer;
}
Loading