diff --git a/packages/http-server/src/__tests__/integration/http-server.integration.ts b/packages/http-server/src/__tests__/integration/http-server.integration.ts index 4379d593de1c..b357eddf99ca 100644 --- a/packages/http-server/src/__tests__/integration/http-server.integration.ts +++ b/packages/http-server/src/__tests__/integration/http-server.integration.ts @@ -6,6 +6,7 @@ import { expect, givenHttpServerConfig, + Http2sOptions, httpGetAsync, httpsGetAsync, skipOnTravis, @@ -18,7 +19,7 @@ import os from 'os'; import pEvent from 'p-event'; import path from 'path'; import {HttpOptions, HttpServer, HttpsOptions} from '../../'; -import {HttpServerOptions} from '../../http-server'; +import {Http2Options, HttpServerOptions} from '../../http-server'; describe('HttpServer (integration)', () => { let server: HttpServer | undefined; @@ -294,6 +295,13 @@ describe('HttpServer (integration)', () => { }).to.throw(/Named pipe test\.pipe does NOT start with/); }); + it('supports HTTP/2 HTTP', async () => { + const http2Server = givenHttp2Server(); + await http2Server.start(); + const response = await httpGetAsync(http2Server.url); + expect(response.statusCode).to.equal(200); + }); + function getAddressFamily(httpServer: HttpServer) { if (!httpServer || !httpServer.address) return undefined; if (typeof httpServer.address === 'string') { @@ -353,4 +361,25 @@ describe('HttpServer (integration)', () => { } return new HttpServer(dummyRequestHandler, options); } + + function givenHttp2Server(): HttpServer { + const options = givenHttpServerConfig({ + protocol: 'http2', + }); + + return new HttpServer(dummyRequestHandler, options); + } + + function givenHttp2sServer({ + allowHttp1 = false, + }: { + allowHttp1: boolean; + }): HttpServer { + const options = givenHttpServerConfig({ + protocol: 'http2s', + allowHTTP1: allowHttp1, + }); + + return new HttpServer(dummyRequestHandler, options); + } }); diff --git a/packages/http-server/src/http-server.ts b/packages/http-server/src/http-server.ts index 255d3adf8d2a..87190ca5fa4a 100644 --- a/packages/http-server/src/http-server.ts +++ b/packages/http-server/src/http-server.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import http, {IncomingMessage, ServerResponse} from 'http'; +import http2 from 'http2'; import https from 'https'; import {AddressInfo, ListenOptions} from 'net'; import os from 'os'; @@ -19,6 +20,14 @@ export type RequestListener = ( res: ServerResponse, ) => void; +/** + * Request listener function for http2/https2 requests + */ +export type Http2RequestListener = ( + req: http2.Http2ServerRequest, + res: http2.Http2ServerResponse, +) => void; + /** * Base options that are common to http and https servers */ @@ -52,17 +61,37 @@ export interface HttpsOptions extends BaseHttpOptions, https.ServerOptions { protocol: 'https'; } +/** + * HTTP/2 HTTP server options + */ +export interface Http2Options extends BaseHttpOptions, http2.ServerOptions { + protocol: 'http2'; +} + +/** + * HTTP/2 HTTPS server options + */ +export interface Https2Options + extends BaseHttpOptions, + http2.SecureServerOptions { + protocol: 'https2'; +} + /** * Possible server options * */ -export type HttpServerOptions = HttpOptions | HttpsOptions; +export type HttpServerOptions = + | HttpOptions + | HttpsOptions + | Http2Options + | Https2Options; /** * Supported protocols * */ -export type HttpProtocol = 'http' | 'https'; // Will be extended to `http2` in the future +export type HttpProtocol = 'http' | 'https' | 'http2' | 'https2'; /** * HTTP / HTTPS server used by LoopBack's RestServer @@ -71,8 +100,12 @@ export class HttpServer { private _listening = false; private _protocol: HttpProtocol; private _address: string | AddressInfo; - private requestListener: RequestListener; - readonly server: http.Server | https.Server; + private requestListener: RequestListener | Http2RequestListener; + readonly server: + | http.Server + | https.Server + | http2.Http2Server + | http2.Http2SecureServer; private _stoppable: stoppable.StoppableServer; private serverOptions: HttpServerOptions; @@ -81,7 +114,7 @@ export class HttpServer { * @param serverOptions */ constructor( - requestListener: RequestListener, + requestListener: RequestListener | Http2RequestListener, serverOptions?: HttpServerOptions, ) { this.requestListener = requestListener; @@ -95,19 +128,47 @@ export class HttpServer { // Remove `port` so that `path` is honored delete this.serverOptions.port; } - 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._protocol = serverOptions?.protocol ?? 'http'; + switch (this._protocol) { + case 'https': + this.server = https.createServer( + this.serverOptions as https.ServerOptions, + this.requestListener as RequestListener, + ); + break; + case 'http': + this.server = http.createServer( + this.requestListener as RequestListener, + ); + break; + case 'http2': + this.server = http2.createServer( + this.requestListener as Http2RequestListener, + ); + break; + case 'https2': + this.server = http2.createSecureServer( + this.requestListener as Http2RequestListener, + ); + break; } // Set up graceful stop for http server if (typeof this.serverOptions.gracePeriodForClose === 'number') { + let serverNormalized; + + switch (this._protocol) { + case 'http': + case 'http2': + serverNormalized = this.server as http.Server; + break; + case 'https': + case 'https2': + serverNormalized = this.server as https.Server; + break; + } + this._stoppable = stoppable( - this.server, + serverNormalized, this.serverOptions.gracePeriodForClose, ); } diff --git a/packages/testlab/src/http-server-config.ts b/packages/testlab/src/http-server-config.ts index 2ce9d9345f89..742e48926090 100644 --- a/packages/testlab/src/http-server-config.ts +++ b/packages/testlab/src/http-server-config.ts @@ -4,6 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {readFileSync} from 'fs'; +import http2 from 'http2'; import {ServerOptions as HttpsServerOptions} from 'https'; import {ListenOptions} from 'net'; import path from 'path'; @@ -22,6 +23,16 @@ export interface HttpsOptions extends ListenOptions, HttpsServerOptions { protocol: 'https'; } +export interface Http2Options extends ListenOptions, http2.ServerOptions { + protocol: 'http2'; +} + +export interface Http2sOptions + extends ListenOptions, + http2.SecureServerOptions { + protocol: 'http2s'; +} + export type HostPort = { host: string; port: number; @@ -36,9 +47,9 @@ export type HostPort = { * * @param customConfig - Additional configuration options to apply. */ -export function givenHttpServerConfig( - customConfig?: T, -): HostPort & T { +export function givenHttpServerConfig< + T extends HttpOptions | HttpsOptions | Http2Options | Http2sOptions +>(customConfig?: T): HostPort & T { const defaults = { host: '127.0.0.1', port: 0,