From d9c7e3dd22ef3dc8de7a2b23c68a0f7bfda092cb Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Mon, 17 Jun 2024 07:38:23 -0400 Subject: [PATCH 1/7] create superclass for all Http Servers --- src/AbstractHttpServer.js | 29 ++++++++++++++++++++++ src/events/alb/HttpServer.js | 19 +++++++------- src/events/http/HttpServer.js | 40 +++++++++++++++--------------- src/events/websocket/HttpServer.js | 18 +++++++------- src/events/websocket/WebSocket.js | 6 ++++- src/lambda/HttpServer.js | 15 +++++------ src/lambda/Lambda.js | 37 +++++++++++++++++++++++---- 7 files changed, 113 insertions(+), 51 deletions(-) create mode 100644 src/AbstractHttpServer.js diff --git a/src/AbstractHttpServer.js b/src/AbstractHttpServer.js new file mode 100644 index 000000000..ee4f0b53e --- /dev/null +++ b/src/AbstractHttpServer.js @@ -0,0 +1,29 @@ +export default class AbstractHttpServer { + #httpServer = null + + #lambda = null + + #port = null + + constructor(lambda, port) { + this.#lambda = lambda + this.#port = port + } + + get httpServer() { + return this.#httpServer + } + + set httpServer(httpServer) { + this.#httpServer = httpServer + this.#lambda.putServer(this.#port, this.#httpServer) + } + + get listener() { + return this.#httpServer.listener + } + + get port() { + return this.#port + } +} diff --git a/src/events/alb/HttpServer.js b/src/events/alb/HttpServer.js index 172509c06..33f111268 100644 --- a/src/events/alb/HttpServer.js +++ b/src/events/alb/HttpServer.js @@ -9,22 +9,23 @@ import { } from "../../utils/index.js" import LambdaAlbRequestEvent from "./lambda-events/LambdaAlbRequestEvent.js" import logRoutes from "../../utils/logRoutes.js" +import AbstractHttpServer from "../../AbstractHttpServer.js" const { stringify } = JSON const { entries } = Object -export default class HttpServer { +export default class HttpServer extends AbstractHttpServer { #lambda = null #options = null #serverless = null - #server = null - #terminalInfo = [] constructor(serverless, options, lambda) { + super(lambda, options.albPort) + this.#serverless = serverless this.#options = options this.#lambda = lambda @@ -43,9 +44,9 @@ export default class HttpServer { }, } - this.#server = new Server(serverOptions) + this.httpServer = new Server(serverOptions) - this.#server.ext("onPreResponse", (request, h) => { + this.httpServer.ext("onPreResponse", (request, h) => { if (request.headers.origin) { const response = request.response.isBoom ? request.response.output @@ -137,7 +138,7 @@ export default class HttpServer { const { albPort, host, httpsProtocol } = this.#options try { - await this.#server.start() + await this.httpServer.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline alb server on port ${albPort}:`, @@ -153,13 +154,13 @@ export default class HttpServer { } stop(timeout) { - return this.#server.stop({ + return this.httpServer.stop({ timeout, }) } get server() { - return this.#server.listener + return this.httpServer.listener } #createHapiHandler(params) { @@ -346,7 +347,7 @@ export default class HttpServer { stage, }) - this.#server.route({ + this.httpServer.route({ handler: hapiHandler, method: hapiMethod, options: hapiOptions, diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 59e07b225..e748093e6 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -30,11 +30,12 @@ import { jsonPath, splitHandlerPathAndName, } from "../../utils/index.js" +import AbstractHttpServer from "../../AbstractHttpServer.js" const { parse, stringify } = JSON const { assign, entries, keys } = Object -export default class HttpServer { +export default class HttpServer extends AbstractHttpServer { #apiKeysValues = null #hasPrivateHttpEvent = false @@ -43,13 +44,12 @@ export default class HttpServer { #options = null - #server = null - #serverless = null #terminalInfo = [] constructor(serverless, options, lambda) { + super(lambda, options.httpPort) this.#lambda = lambda this.#options = options this.#serverless = serverless @@ -95,16 +95,16 @@ export default class HttpServer { } // Hapijs server creation - this.#server = new Server(serverOptions) + this.httpServer = new Server(serverOptions) try { - await this.#server.register([h2o2]) + await this.httpServer.register([h2o2]) } catch (err) { log.error(err) } // Enable CORS preflight response - this.#server.ext("onPreResponse", (request, h) => { + this.httpServer.ext("onPreResponse", (request, h) => { if (request.headers.origin) { const response = request.response.isBoom ? request.response.output @@ -196,7 +196,7 @@ export default class HttpServer { const { host, httpPort, httpsProtocol } = this.#options try { - await this.#server.start() + await this.httpServer.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline server on port ${httpPort}:`, @@ -213,7 +213,7 @@ export default class HttpServer { // stops the server stop(timeout) { - return this.#server.stop({ + return this.httpServer.stop({ timeout, }) } @@ -278,8 +278,8 @@ export default class HttpServer { const scheme = createJWTAuthScheme(jwtSettings) // Set the auth scheme and strategy on the server - this.#server.auth.scheme(authSchemeName, scheme) - this.#server.auth.strategy(authStrategyName, authSchemeName) + this.httpServer.auth.scheme(authSchemeName, scheme) + this.httpServer.auth.strategy(authStrategyName, authSchemeName) return authStrategyName } @@ -387,8 +387,8 @@ export default class HttpServer { ) // Set the auth scheme and strategy on the server - this.#server.auth.scheme(authSchemeName, scheme) - this.#server.auth.strategy(authStrategyName, authSchemeName) + this.httpServer.auth.scheme(authSchemeName, scheme) + this.httpServer.auth.strategy(authStrategyName, authSchemeName) return authStrategyName } @@ -416,11 +416,11 @@ export default class HttpServer { const strategy = provider(endpoint, functionKey, method, path) - this.#server.auth.scheme( + this.httpServer.auth.scheme( strategy.scheme, strategy.getAuthenticateFunction, ) - this.#server.auth.strategy(strategy.name, strategy.scheme) + this.httpServer.auth.strategy(strategy.name, strategy.scheme) return strategy.name } @@ -1118,7 +1118,7 @@ export default class HttpServer { stage, }) - this.#server.route({ + this.httpServer.route({ handler: hapiHandler, method: hapiMethod, options: hapiOptions, @@ -1267,17 +1267,17 @@ export default class HttpServer { path: hapiPath, } - this.#server.route(route) + this.httpServer.route(route) }) } create404Route() { // If a {proxy+} or $default route exists, don't conflict with it - if (this.#server.match("*", "/{p*}")) { + if (this.httpServer.match("*", "/{p*}")) { return } - const existingRoutes = this.#server + const existingRoutes = this.httpServer .table() // Exclude this (404) route .filter((route) => route.path !== "/{p*}") @@ -1305,7 +1305,7 @@ export default class HttpServer { path: "/{p*}", } - this.#server.route(route) + this.httpServer.route(route) } #getArrayStackTrace(stack) { @@ -1329,6 +1329,6 @@ export default class HttpServer { // TEMP FIXME quick fix to expose gateway server for testing, look for better solution getServer() { - return this.#server + return this.httpServer } } diff --git a/src/events/websocket/HttpServer.js b/src/events/websocket/HttpServer.js index c50289398..2e2ded21a 100644 --- a/src/events/websocket/HttpServer.js +++ b/src/events/websocket/HttpServer.js @@ -3,16 +3,16 @@ import { resolve } from "node:path" import { exit } from "node:process" import { Server } from "@hapi/hapi" import { log } from "../../utils/log.js" +import AbstractHttpServer from "../../AbstractHttpServer.js" import { catchAllRoute, connectionsRoutes } from "./http-routes/index.js" -export default class HttpServer { +export default class HttpServer extends AbstractHttpServer { #options = null - #server = null - #webSocketClients = null - constructor(options, webSocketClients) { + constructor(options, lambda, webSocketClients) { + super(lambda, options.websocketPort) this.#options = options this.#webSocketClients = webSocketClients } @@ -44,7 +44,7 @@ export default class HttpServer { }), } - this.#server = new Server(serverOptions) + this.httpServer = new Server(serverOptions) } async start() { @@ -53,12 +53,12 @@ export default class HttpServer { ...connectionsRoutes(this.#webSocketClients), catchAllRoute(), ] - this.#server.route(routes) + this.httpServer.route(routes) const { host, httpsProtocol, websocketPort } = this.#options try { - await this.#server.start() + await this.httpServer.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline websocket server on port ${websocketPort}:`, @@ -76,12 +76,12 @@ export default class HttpServer { // stops the server stop(timeout) { - return this.#server.stop({ + return this.httpServer.stop({ timeout, }) } get server() { - return this.#server.listener + return this.httpServer.listener } } diff --git a/src/events/websocket/WebSocket.js b/src/events/websocket/WebSocket.js index 19b543855..3337f8524 100644 --- a/src/events/websocket/WebSocket.js +++ b/src/events/websocket/WebSocket.js @@ -27,7 +27,11 @@ export default class WebSocket { this.#lambda, ) - this.#httpServer = new HttpServer(this.#options, webSocketClients) + this.#httpServer = new HttpServer( + this.#options, + this.#lambda, + webSocketClients, + ) await this.#httpServer.createServer() diff --git a/src/lambda/HttpServer.js b/src/lambda/HttpServer.js index d2eb946e2..097ed8d0f 100644 --- a/src/lambda/HttpServer.js +++ b/src/lambda/HttpServer.js @@ -2,15 +2,16 @@ import { exit } from "node:process" import { Server } from "@hapi/hapi" import { log } from "../utils/log.js" import { invocationsRoute, invokeAsyncRoute } from "./routes/index.js" +import AbstractHttpServer from "../AbstractHttpServer.js" -export default class HttpServer { +export default class HttpServer extends AbstractHttpServer { #lambda = null #options = null - #server = null - constructor(options, lambda) { + super(lambda, options.lambdaPort) + this.#lambda = lambda this.#options = options @@ -21,7 +22,7 @@ export default class HttpServer { port: lambdaPort, } - this.#server = new Server(serverOptions) + this.httpServer = new Server(serverOptions) } async start() { @@ -29,12 +30,12 @@ export default class HttpServer { const invRoute = invocationsRoute(this.#lambda, this.#options) const invAsyncRoute = invokeAsyncRoute(this.#lambda, this.#options) - this.#server.route([invAsyncRoute, invRoute]) + this.httpServer.route([invAsyncRoute, invRoute]) const { host, httpsProtocol, lambdaPort } = this.#options try { - await this.#server.start() + await this.httpServer.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline lambda server on port ${lambdaPort}:`, @@ -103,7 +104,7 @@ export default class HttpServer { // stops the server stop(timeout) { - return this.#server.stop({ + return this.httpServer.stop({ timeout, }) } diff --git a/src/lambda/Lambda.js b/src/lambda/Lambda.js index 779a8ef35..fe5827ab1 100644 --- a/src/lambda/Lambda.js +++ b/src/lambda/Lambda.js @@ -4,7 +4,9 @@ import LambdaFunctionPool from "./LambdaFunctionPool.js" const { assign } = Object export default class Lambda { - #httpServer = null + #httpServers = new Map() + + #options = null #lambdas = new Map() @@ -13,8 +15,12 @@ export default class Lambda { #lambdaFunctionPool = null constructor(serverless, options) { - this.#httpServer = new HttpServer(options, this) - this.#lambdaFunctionPool = new LambdaFunctionPool(serverless, options) + this.#options = options + this.putServer( + this.#options.lambdaPort, + new HttpServer(this.#options, this), + ) + this.#lambdaFunctionPool = new LambdaFunctionPool(serverless, this.#options) } #createEvent(functionKey, functionDefinition) { @@ -54,15 +60,36 @@ export default class Lambda { start() { this.#lambdaFunctionPool.start() - return this.#httpServer.start() + return this.getServer(this.#options.lambdaPort).start() } // stops the server stop(timeout) { - return this.#httpServer.stop(timeout) + return this.getServer(this.#options.lambdaPort).stop(timeout) } cleanup() { return this.#lambdaFunctionPool.cleanup() } + + putServer(port, server) { + this.#httpServers.set(port, server) + } + + getServer(port) { + return this.#httpServers.get(port) + } + + getServerAsync(port) { + return new Promise((resolve) => { + const server = this.#httpServers.get(port) + if (server) { + resolve(server) + } else { + setTimeout(() => { + resolve(this.getServerAsync(port)) + }, 10) + } + }) + } } From b02a980e86095b9eda8ace1caaa2a83617c0db83 Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Mon, 17 Jun 2024 07:52:47 -0400 Subject: [PATCH 2/7] start/stop servers using super --- src/AbstractHttpServer.js | 25 +++++++++++++++++++++++++ src/events/alb/HttpServer.js | 4 ++-- src/events/http/HttpServer.js | 4 ++-- src/events/websocket/HttpServer.js | 4 ++-- src/lambda/HttpServer.js | 4 ++-- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/AbstractHttpServer.js b/src/AbstractHttpServer.js index ee4f0b53e..3cfb8db71 100644 --- a/src/AbstractHttpServer.js +++ b/src/AbstractHttpServer.js @@ -5,11 +5,36 @@ export default class AbstractHttpServer { #port = null + #additionalRoutes = [] + + #started = false + constructor(lambda, port) { this.#lambda = lambda this.#port = port } + start() { + if (this.#started) { + return Promise.resolve() + } + this.#started = true + this.#httpServer.route(this.#additionalRoutes) + return this.#httpServer.start() + } + + stop(timeout) { + if (!this.#started) { + return Promise.resolve() + } + this.#started = false + return this.#httpServer.stop(timeout) + } + + addRoutes(routes) { + this.#additionalRoutes = this.#additionalRoutes.push(...routes) + } + get httpServer() { return this.#httpServer } diff --git a/src/events/alb/HttpServer.js b/src/events/alb/HttpServer.js index 33f111268..06efe7cb5 100644 --- a/src/events/alb/HttpServer.js +++ b/src/events/alb/HttpServer.js @@ -138,7 +138,7 @@ export default class HttpServer extends AbstractHttpServer { const { albPort, host, httpsProtocol } = this.#options try { - await this.httpServer.start() + await super.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline alb server on port ${albPort}:`, @@ -154,7 +154,7 @@ export default class HttpServer extends AbstractHttpServer { } stop(timeout) { - return this.httpServer.stop({ + return super.stop({ timeout, }) } diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index e748093e6..02084c5b7 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -196,7 +196,7 @@ export default class HttpServer extends AbstractHttpServer { const { host, httpPort, httpsProtocol } = this.#options try { - await this.httpServer.start() + await super.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline server on port ${httpPort}:`, @@ -213,7 +213,7 @@ export default class HttpServer extends AbstractHttpServer { // stops the server stop(timeout) { - return this.httpServer.stop({ + return super.stop({ timeout, }) } diff --git a/src/events/websocket/HttpServer.js b/src/events/websocket/HttpServer.js index 2e2ded21a..1150ff6a9 100644 --- a/src/events/websocket/HttpServer.js +++ b/src/events/websocket/HttpServer.js @@ -58,7 +58,7 @@ export default class HttpServer extends AbstractHttpServer { const { host, httpsProtocol, websocketPort } = this.#options try { - await this.httpServer.start() + await super.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline websocket server on port ${websocketPort}:`, @@ -76,7 +76,7 @@ export default class HttpServer extends AbstractHttpServer { // stops the server stop(timeout) { - return this.httpServer.stop({ + return super.stop({ timeout, }) } diff --git a/src/lambda/HttpServer.js b/src/lambda/HttpServer.js index 097ed8d0f..6c8b259d4 100644 --- a/src/lambda/HttpServer.js +++ b/src/lambda/HttpServer.js @@ -35,7 +35,7 @@ export default class HttpServer extends AbstractHttpServer { const { host, httpsProtocol, lambdaPort } = this.#options try { - await this.httpServer.start() + await super.start() } catch (err) { log.error( `Unexpected error while starting serverless-offline lambda server on port ${lambdaPort}:`, @@ -104,7 +104,7 @@ export default class HttpServer extends AbstractHttpServer { // stops the server stop(timeout) { - return this.httpServer.stop({ + return super.stop({ timeout, }) } From 4c486ebce51400696cb65c8b00ffda05ac0ddbad Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Tue, 18 Jun 2024 06:44:16 -0400 Subject: [PATCH 3/7] use lambda exclusively for hapi server storage --- src/AbstractHttpServer.js | 65 +++++++++++++++++++++--------- src/events/alb/HttpServer.js | 17 +------- src/events/http/HttpServer.js | 46 +-------------------- src/events/websocket/HttpServer.js | 33 +-------------- src/lambda/HttpServer.js | 13 ++---- src/lambda/Lambda.js | 25 +++--------- 6 files changed, 59 insertions(+), 140 deletions(-) diff --git a/src/AbstractHttpServer.js b/src/AbstractHttpServer.js index 3cfb8db71..533d14431 100644 --- a/src/AbstractHttpServer.js +++ b/src/AbstractHttpServer.js @@ -1,17 +1,54 @@ -export default class AbstractHttpServer { - #httpServer = null +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import { Server } from "@hapi/hapi" + +function loadCerts(httpsProtocol) { + return { + cert: readFileSync(resolve(httpsProtocol, "cert.pem"), "utf8"), + key: readFileSync(resolve(httpsProtocol, "key.pem"), "utf8"), + } +} +export default class AbstractHttpServer { #lambda = null #port = null - #additionalRoutes = [] - #started = false - constructor(lambda, port) { + constructor(lambda, options, port) { this.#lambda = lambda this.#port = port + + if (this.#lambda.getServer(port)) { + return + } + + const { host, httpsProtocol, enforceSecureCookies } = options + + const server = new Server({ + host, + port, + router: { + stripTrailingSlash: true, + }, + state: enforceSecureCookies + ? { + isHttpOnly: true, + isSameSite: false, + isSecure: true, + } + : { + isHttpOnly: false, + isSameSite: false, + isSecure: false, + }, + ...(httpsProtocol != null && { + tls: loadCerts(httpsProtocol), + }), + }) + + this.#lambda.putServer(port, server) } start() { @@ -19,8 +56,7 @@ export default class AbstractHttpServer { return Promise.resolve() } this.#started = true - this.#httpServer.route(this.#additionalRoutes) - return this.#httpServer.start() + return this.httpServer.start() } stop(timeout) { @@ -28,24 +64,15 @@ export default class AbstractHttpServer { return Promise.resolve() } this.#started = false - return this.#httpServer.stop(timeout) - } - - addRoutes(routes) { - this.#additionalRoutes = this.#additionalRoutes.push(...routes) + return this.httpServer.stop(timeout) } get httpServer() { - return this.#httpServer - } - - set httpServer(httpServer) { - this.#httpServer = httpServer - this.#lambda.putServer(this.#port, this.#httpServer) + return this.#lambda.getServer(this.port) } get listener() { - return this.#httpServer.listener + return this.httpServer.listener } get port() { diff --git a/src/events/alb/HttpServer.js b/src/events/alb/HttpServer.js index 06efe7cb5..6fcf0f170 100644 --- a/src/events/alb/HttpServer.js +++ b/src/events/alb/HttpServer.js @@ -1,6 +1,5 @@ import { Buffer } from "node:buffer" import { exit } from "node:process" -import { Server } from "@hapi/hapi" import { log } from "../../utils/log.js" import { detectEncoding, @@ -24,7 +23,7 @@ export default class HttpServer extends AbstractHttpServer { #terminalInfo = [] constructor(serverless, options, lambda) { - super(lambda, options.albPort) + super(lambda, options, options.albPort) this.#serverless = serverless this.#options = options @@ -32,20 +31,6 @@ export default class HttpServer extends AbstractHttpServer { } async createServer() { - const { host, albPort } = this.#options - - const serverOptions = { - host, - port: albPort, - router: { - // allows for paths with trailing slashes to be the same as without - // e.g. : /my-path is the same as /my-path/ - stripTrailingSlash: true, - }, - } - - this.httpServer = new Server(serverOptions) - this.httpServer.ext("onPreResponse", (request, h) => { if (request.headers.origin) { const response = request.response.isBoom diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 02084c5b7..65e5d9a3e 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -1,10 +1,9 @@ import { Buffer } from "node:buffer" -import { readFile } from "node:fs/promises" + import { createRequire } from "node:module" import { join, resolve } from "node:path" import { exit } from "node:process" import h2o2 from "@hapi/h2o2" -import { Server } from "@hapi/hapi" import { log } from "../../utils/log.js" import authFunctionNameExtractor from "../authFunctionNameExtractor.js" import authJWTSettingsExtractor from "./authJWTSettingsExtractor.js" @@ -49,54 +48,13 @@ export default class HttpServer extends AbstractHttpServer { #terminalInfo = [] constructor(serverless, options, lambda) { - super(lambda, options.httpPort) + super(lambda, options, options.httpPort) this.#lambda = lambda this.#options = options this.#serverless = serverless } - async #loadCerts(httpsProtocol) { - const [cert, key] = await Promise.all([ - readFile(resolve(httpsProtocol, "cert.pem"), "utf8"), - readFile(resolve(httpsProtocol, "key.pem"), "utf8"), - ]) - - return { - cert, - key, - } - } - async createServer() { - const { enforceSecureCookies, host, httpPort, httpsProtocol } = - this.#options - - const serverOptions = { - host, - port: httpPort, - router: { - stripTrailingSlash: true, - }, - state: enforceSecureCookies - ? { - isHttpOnly: true, - isSameSite: false, - isSecure: true, - } - : { - isHttpOnly: false, - isSameSite: false, - isSecure: false, - }, - // https support - ...(httpsProtocol != null && { - tls: await this.#loadCerts(httpsProtocol), - }), - } - - // Hapijs server creation - this.httpServer = new Server(serverOptions) - try { await this.httpServer.register([h2o2]) } catch (err) { diff --git a/src/events/websocket/HttpServer.js b/src/events/websocket/HttpServer.js index 1150ff6a9..59c0504b4 100644 --- a/src/events/websocket/HttpServer.js +++ b/src/events/websocket/HttpServer.js @@ -1,7 +1,4 @@ -import { readFile } from "node:fs/promises" -import { resolve } from "node:path" import { exit } from "node:process" -import { Server } from "@hapi/hapi" import { log } from "../../utils/log.js" import AbstractHttpServer from "../../AbstractHttpServer.js" import { catchAllRoute, connectionsRoutes } from "./http-routes/index.js" @@ -12,39 +9,13 @@ export default class HttpServer extends AbstractHttpServer { #webSocketClients = null constructor(options, lambda, webSocketClients) { - super(lambda, options.websocketPort) + super(lambda, options, options.websocketPort) this.#options = options this.#webSocketClients = webSocketClients } - async #loadCerts(httpsProtocol) { - const [cert, key] = await Promise.all([ - readFile(resolve(httpsProtocol, "cert.pem"), "utf8"), - readFile(resolve(httpsProtocol, "key.pem"), "utf8"), - ]) - - return { - cert, - key, - } - } - async createServer() { - const { host, httpsProtocol, websocketPort } = this.#options - - const serverOptions = { - host, - port: websocketPort, - router: { - stripTrailingSlash: true, - }, - // https support - ...(httpsProtocol != null && { - tls: await this.#loadCerts(httpsProtocol), - }), - } - - this.httpServer = new Server(serverOptions) + // No-op } async start() { diff --git a/src/lambda/HttpServer.js b/src/lambda/HttpServer.js index 6c8b259d4..0abfc88a6 100644 --- a/src/lambda/HttpServer.js +++ b/src/lambda/HttpServer.js @@ -1,5 +1,4 @@ import { exit } from "node:process" -import { Server } from "@hapi/hapi" import { log } from "../utils/log.js" import { invocationsRoute, invokeAsyncRoute } from "./routes/index.js" import AbstractHttpServer from "../AbstractHttpServer.js" @@ -10,19 +9,13 @@ export default class HttpServer extends AbstractHttpServer { #options = null constructor(options, lambda) { - super(lambda, options.lambdaPort) + super(lambda, options, options.lambdaPort) this.#lambda = lambda this.#options = options - const { host, lambdaPort } = options - - const serverOptions = { - host, - port: lambdaPort, - } - - this.httpServer = new Server(serverOptions) + // disable the default stripTrailingSlash + this.httpServer.settings.router.stripTrailingSlash = false } async start() { diff --git a/src/lambda/Lambda.js b/src/lambda/Lambda.js index fe5827ab1..04b4fce53 100644 --- a/src/lambda/Lambda.js +++ b/src/lambda/Lambda.js @@ -4,6 +4,8 @@ import LambdaFunctionPool from "./LambdaFunctionPool.js" const { assign } = Object export default class Lambda { + #httpServer = null + #httpServers = new Map() #options = null @@ -15,11 +17,8 @@ export default class Lambda { #lambdaFunctionPool = null constructor(serverless, options) { + this.#httpServer = new HttpServer(options, this) this.#options = options - this.putServer( - this.#options.lambdaPort, - new HttpServer(this.#options, this), - ) this.#lambdaFunctionPool = new LambdaFunctionPool(serverless, this.#options) } @@ -59,13 +58,12 @@ export default class Lambda { start() { this.#lambdaFunctionPool.start() - - return this.getServer(this.#options.lambdaPort).start() + return this.#httpServer.start() } // stops the server stop(timeout) { - return this.getServer(this.#options.lambdaPort).stop(timeout) + return this.#httpServer.stop(timeout) } cleanup() { @@ -79,17 +77,4 @@ export default class Lambda { getServer(port) { return this.#httpServers.get(port) } - - getServerAsync(port) { - return new Promise((resolve) => { - const server = this.#httpServers.get(port) - if (server) { - resolve(server) - } else { - setTimeout(() => { - resolve(this.getServerAsync(port)) - }, 10) - } - }) - } } From dfeffa0d8bc37ab0dbe5894100d272484a898a0d Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Tue, 18 Jun 2024 07:23:40 -0400 Subject: [PATCH 4/7] move most stop/start into abstract http server --- src/events/alb/HttpServer.js | 26 ++----------- src/events/http/HttpServer.js | 27 ++------------ src/events/websocket/HttpServer.js | 33 ++--------------- src/{ => lambda}/AbstractHttpServer.js | 51 ++++++++++++++++++++++++-- src/lambda/HttpServer.js | 37 +++---------------- src/lambda/Lambda.js | 2 +- 6 files changed, 62 insertions(+), 114 deletions(-) rename src/{ => lambda}/AbstractHttpServer.js (58%) diff --git a/src/events/alb/HttpServer.js b/src/events/alb/HttpServer.js index 6fcf0f170..971e8df8a 100644 --- a/src/events/alb/HttpServer.js +++ b/src/events/alb/HttpServer.js @@ -1,5 +1,4 @@ import { Buffer } from "node:buffer" -import { exit } from "node:process" import { log } from "../../utils/log.js" import { detectEncoding, @@ -8,7 +7,7 @@ import { } from "../../utils/index.js" import LambdaAlbRequestEvent from "./lambda-events/LambdaAlbRequestEvent.js" import logRoutes from "../../utils/logRoutes.js" -import AbstractHttpServer from "../../AbstractHttpServer.js" +import AbstractHttpServer from "../../lambda/AbstractHttpServer.js" const { stringify } = JSON const { entries } = Object @@ -120,28 +119,9 @@ export default class HttpServer extends AbstractHttpServer { } async start() { - const { albPort, host, httpsProtocol } = this.#options - - try { - await super.start() - } catch (err) { - log.error( - `Unexpected error while starting serverless-offline alb server on port ${albPort}:`, - err, - ) - exit(1) - } - - // TODO move the following block - const server = `${httpsProtocol ? "https" : "http"}://${host}:${albPort}` + await super.start() - log.notice(`ALB Server ready: ${server} 🚀`) - } - - stop(timeout) { - return super.stop({ - timeout, - }) + log.notice(`${this.serverName} Server ready: ${this.basePath} 🚀`) } get server() { diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 65e5d9a3e..b8094105d 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -2,7 +2,6 @@ import { Buffer } from "node:buffer" import { createRequire } from "node:module" import { join, resolve } from "node:path" -import { exit } from "node:process" import h2o2 from "@hapi/h2o2" import { log } from "../../utils/log.js" import authFunctionNameExtractor from "../authFunctionNameExtractor.js" @@ -29,7 +28,7 @@ import { jsonPath, splitHandlerPathAndName, } from "../../utils/index.js" -import AbstractHttpServer from "../../AbstractHttpServer.js" +import AbstractHttpServer from "../../lambda/AbstractHttpServer.js" const { parse, stringify } = JSON const { assign, entries, keys } = Object @@ -151,29 +150,9 @@ export default class HttpServer extends AbstractHttpServer { } async start() { - const { host, httpPort, httpsProtocol } = this.#options - - try { - await super.start() - } catch (err) { - log.error( - `Unexpected error while starting serverless-offline server on port ${httpPort}:`, - err, - ) - exit(1) - } - - // TODO move the following block - const server = `${httpsProtocol ? "https" : "http"}://${host}:${httpPort}` + await super.start() - log.notice(`Server ready: ${server} 🚀`) - } - - // stops the server - stop(timeout) { - return super.stop({ - timeout, - }) + log.notice(`Server ready: ${this.basePath} 🚀`) } #logPluginIssue() { diff --git a/src/events/websocket/HttpServer.js b/src/events/websocket/HttpServer.js index 59c0504b4..47026bad8 100644 --- a/src/events/websocket/HttpServer.js +++ b/src/events/websocket/HttpServer.js @@ -1,16 +1,11 @@ -import { exit } from "node:process" -import { log } from "../../utils/log.js" -import AbstractHttpServer from "../../AbstractHttpServer.js" +import AbstractHttpServer from "../../lambda/AbstractHttpServer.js" import { catchAllRoute, connectionsRoutes } from "./http-routes/index.js" export default class HttpServer extends AbstractHttpServer { - #options = null - #webSocketClients = null constructor(options, lambda, webSocketClients) { super(lambda, options, options.websocketPort) - this.#options = options this.#webSocketClients = webSocketClients } @@ -24,32 +19,10 @@ export default class HttpServer extends AbstractHttpServer { ...connectionsRoutes(this.#webSocketClients), catchAllRoute(), ] - this.httpServer.route(routes) - - const { host, httpsProtocol, websocketPort } = this.#options - try { - await super.start() - } catch (err) { - log.error( - `Unexpected error while starting serverless-offline websocket server on port ${websocketPort}:`, - err, - ) - exit(1) - } - - log.notice( - `Offline [http for websocket] listening on ${ - httpsProtocol ? "https" : "http" - }://${host}:${websocketPort}`, - ) - } + this.httpServer.route(routes) - // stops the server - stop(timeout) { - return super.stop({ - timeout, - }) + await super.start() } get server() { diff --git a/src/AbstractHttpServer.js b/src/lambda/AbstractHttpServer.js similarity index 58% rename from src/AbstractHttpServer.js rename to src/lambda/AbstractHttpServer.js index 533d14431..51fff9618 100644 --- a/src/AbstractHttpServer.js +++ b/src/lambda/AbstractHttpServer.js @@ -1,6 +1,8 @@ +import { exit } from "node:process" import { readFileSync } from "node:fs" import { resolve } from "node:path" import { Server } from "@hapi/hapi" +import { log } from "../utils/log.js" function loadCerts(httpsProtocol) { return { @@ -12,12 +14,15 @@ function loadCerts(httpsProtocol) { export default class AbstractHttpServer { #lambda = null + #options = null + #port = null #started = false constructor(lambda, options, port) { this.#lambda = lambda + this.#options = options this.#port = port if (this.#lambda.getServer(port)) { @@ -51,12 +56,25 @@ export default class AbstractHttpServer { this.#lambda.putServer(port, server) } - start() { + async start() { if (this.#started) { - return Promise.resolve() + return } this.#started = true - return this.httpServer.start() + + try { + await this.httpServer.start() + } catch (err) { + log.error( + `Unexpected error while starting serverless-offline ${this.serverName} server on port ${this.port}:`, + err, + ) + exit(1) + } + + log.notice( + `Offline [http for ${this.serverName}] listening on ${this.basePath}`, + ) } stop(timeout) { @@ -64,7 +82,7 @@ export default class AbstractHttpServer { return Promise.resolve() } this.#started = false - return this.httpServer.stop(timeout) + return this.httpServer.stop({ timeout }) } get httpServer() { @@ -78,4 +96,29 @@ export default class AbstractHttpServer { get port() { return this.#port } + + get basePath() { + const { host, httpsProtocol } = this.#options + return `${httpsProtocol ? "https" : "http"}://${host}:${this.port}` + } + + get serverName() { + if (this.port === this.#options.lambdaPort) { + return "lambda" + } + + if (this.port === this.#options.httpPort) { + return "api gateway" + } + + if (this.port === this.#options.websocketPort) { + return "websocket" + } + + if (this.port === this.#options.albPort) { + return "alb" + } + + return "unknown" + } } diff --git a/src/lambda/HttpServer.js b/src/lambda/HttpServer.js index 0abfc88a6..24fae8323 100644 --- a/src/lambda/HttpServer.js +++ b/src/lambda/HttpServer.js @@ -1,14 +1,13 @@ -import { exit } from "node:process" import { log } from "../utils/log.js" import { invocationsRoute, invokeAsyncRoute } from "./routes/index.js" -import AbstractHttpServer from "../AbstractHttpServer.js" +import AbstractHttpServer from "./AbstractHttpServer.js" export default class HttpServer extends AbstractHttpServer { #lambda = null #options = null - constructor(options, lambda) { + constructor(lambda, options) { super(lambda, options, options.lambdaPort) this.#lambda = lambda @@ -25,28 +24,9 @@ export default class HttpServer extends AbstractHttpServer { this.httpServer.route([invAsyncRoute, invRoute]) - const { host, httpsProtocol, lambdaPort } = this.#options - - try { - await super.start() - } catch (err) { - log.error( - `Unexpected error while starting serverless-offline lambda server on port ${lambdaPort}:`, - err, - ) - exit(1) - } - - log.notice( - `Offline [http for lambda] listening on ${ - httpsProtocol ? "https" : "http" - }://${host}:${lambdaPort}`, - ) + await super.start() // Print all the invocation routes to debug - const basePath = `${ - httpsProtocol ? "https" : "http" - }://${host}:${lambdaPort}` const funcNamePairs = this.#lambda.listFunctionNamePairs() log.notice( @@ -69,7 +49,7 @@ export default class HttpServer extends AbstractHttpServer { (functionName) => ` * ${ invRoute.method - } ${basePath}${invRoute.path.replace( + } ${this.basePath}${invRoute.path.replace( "{functionName}", functionName, )}`, @@ -86,7 +66,7 @@ export default class HttpServer extends AbstractHttpServer { (functionName) => ` * ${ invAsyncRoute.method - } ${basePath}${invAsyncRoute.path.replace( + } ${this.basePath}${invAsyncRoute.path.replace( "{functionName}", functionName, )}`, @@ -94,11 +74,4 @@ export default class HttpServer extends AbstractHttpServer { ].join("\n"), ) } - - // stops the server - stop(timeout) { - return super.stop({ - timeout, - }) - } } diff --git a/src/lambda/Lambda.js b/src/lambda/Lambda.js index 04b4fce53..db19fd79d 100644 --- a/src/lambda/Lambda.js +++ b/src/lambda/Lambda.js @@ -17,7 +17,7 @@ export default class Lambda { #lambdaFunctionPool = null constructor(serverless, options) { - this.#httpServer = new HttpServer(options, this) + this.#httpServer = new HttpServer(this, options) this.#options = options this.#lambdaFunctionPool = new LambdaFunctionPool(serverless, this.#options) } From a234a372e63107b015acfb0b4ad1b9e770f04135 Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Tue, 18 Jun 2024 08:34:14 -0400 Subject: [PATCH 5/7] allow websocket port to be http port --- src/events/websocket/HttpServer.js | 45 ++++++-- src/events/websocket/WebSocket.js | 2 +- src/events/websocket/WebSocketServer.js | 8 +- .../websocket-oneway-shared/serverless.yml | 35 ++++++ .../websocket-oneway-shared/src/handler.js | 19 ++++ .../websocket-oneway-shared/src/package.json | 3 + .../websocket-oneway.test.js | 55 ++++++++++ .../websocket-twoway-shared/serverless.yml | 37 +++++++ .../websocket-twoway-shared/src/handler.js | 32 ++++++ .../websocket-twoway-shared/src/package.json | 3 + .../websocket-twoway.test.js | 102 ++++++++++++++++++ 11 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 tests/integration/websocket-oneway-shared/serverless.yml create mode 100644 tests/integration/websocket-oneway-shared/src/handler.js create mode 100644 tests/integration/websocket-oneway-shared/src/package.json create mode 100644 tests/integration/websocket-oneway-shared/websocket-oneway.test.js create mode 100644 tests/integration/websocket-twoway-shared/serverless.yml create mode 100644 tests/integration/websocket-twoway-shared/src/handler.js create mode 100644 tests/integration/websocket-twoway-shared/src/package.json create mode 100644 tests/integration/websocket-twoway-shared/websocket-twoway.test.js diff --git a/src/events/websocket/HttpServer.js b/src/events/websocket/HttpServer.js index 47026bad8..2edf9d7ef 100644 --- a/src/events/websocket/HttpServer.js +++ b/src/events/websocket/HttpServer.js @@ -2,10 +2,16 @@ import AbstractHttpServer from "../../lambda/AbstractHttpServer.js" import { catchAllRoute, connectionsRoutes } from "./http-routes/index.js" export default class HttpServer extends AbstractHttpServer { + #options = null + + #lambda = null + #webSocketClients = null constructor(options, lambda, webSocketClients) { super(lambda, options, options.websocketPort) + this.#options = options + this.#lambda = lambda this.#webSocketClients = webSocketClients } @@ -15,17 +21,42 @@ export default class HttpServer extends AbstractHttpServer { async start() { // add routes - const routes = [ - ...connectionsRoutes(this.#webSocketClients), - catchAllRoute(), - ] + this.httpServer.route(connectionsRoutes(this.#webSocketClients)) - this.httpServer.route(routes) + if (this.#options.websocketPort !== this.#options.httpPort) { + this.httpServer.route([catchAllRoute()]) + } await super.start() } - get server() { - return this.httpServer.listener + async stop(timeout) { + if (this.#options.websocketPort === this.#options.httpPort) { + return + } + + await super.stop(timeout) + } + + get listener() { + return this.#lambda.getServer(this.#options.websocketPort).listener + } + + get httpServer() { + return this.#lambda.getServer( + this.#options.websocketPort === this.#options.httpPort + ? this.#options.lambdaPort + : this.#options.websocketPort, + ) + } + + get serverName() { + return "websocket" + } + + get port() { + return this.#options.websocketPort === this.#options.httpPort + ? this.#options.lambdaPort + : this.#options.websocketPort } } diff --git a/src/events/websocket/WebSocket.js b/src/events/websocket/WebSocket.js index 3337f8524..f0d5c75e3 100644 --- a/src/events/websocket/WebSocket.js +++ b/src/events/websocket/WebSocket.js @@ -39,7 +39,7 @@ export default class WebSocket { this.#webSocketServer = new WebSocketServer( this.#options, webSocketClients, - this.#httpServer.server, + this.#httpServer.listener, ) await this.#webSocketServer.createServer() diff --git a/src/events/websocket/WebSocketServer.js b/src/events/websocket/WebSocketServer.js index a421f5a4c..4fbb731df 100644 --- a/src/events/websocket/WebSocketServer.js +++ b/src/events/websocket/WebSocketServer.js @@ -7,19 +7,19 @@ export default class WebSocketServer { #options = null - #sharedServer = null + #sharedListener = null #webSocketClients = null - constructor(options, webSocketClients, sharedServer) { + constructor(options, webSocketClients, sharedListener) { this.#options = options - this.#sharedServer = sharedServer + this.#sharedListener = sharedListener this.#webSocketClients = webSocketClients } async createServer() { const server = new WsWebSocketServer({ - server: this.#sharedServer, + server: this.#sharedListener, verifyClient: async ({ req }, cb) => { const connectionId = crypto.randomUUID() const key = req.headers["sec-websocket-key"] diff --git a/tests/integration/websocket-oneway-shared/serverless.yml b/tests/integration/websocket-oneway-shared/serverless.yml new file mode 100644 index 000000000..41bf43e75 --- /dev/null +++ b/tests/integration/websocket-oneway-shared/serverless.yml @@ -0,0 +1,35 @@ +service: oneway-shared-websocket-tests + +configValidationMode: error +deprecationNotificationMode: error + +plugins: + - ../../../src/index.js + +provider: + architecture: arm64 + deploymentMethod: direct + memorySize: 1024 + name: aws + region: us-east-1 + runtime: nodejs18.x + stage: dev + versionFunctions: false + +functions: + handler: + events: + - http: + method: get + path: echo + - websocket: + route: $connect + - websocket: + route: $disconnect + - websocket: + route: $default + handler: src/handler.handler + +custom: + serverless-offline: + websocketPort: 3000 diff --git a/tests/integration/websocket-oneway-shared/src/handler.js b/tests/integration/websocket-oneway-shared/src/handler.js new file mode 100644 index 000000000..6d234640d --- /dev/null +++ b/tests/integration/websocket-oneway-shared/src/handler.js @@ -0,0 +1,19 @@ +const { parse } = JSON + +export async function handler(event) { + const { body, requestContext } = event + + if ( + body && + parse(body).throwError && + requestContext && + requestContext.routeKey === "$default" + ) { + throw new Error("Throwing error from incoming message") + } + + return { + body: body || undefined, + statusCode: 200, + } +} diff --git a/tests/integration/websocket-oneway-shared/src/package.json b/tests/integration/websocket-oneway-shared/src/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/integration/websocket-oneway-shared/src/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/integration/websocket-oneway-shared/websocket-oneway.test.js b/tests/integration/websocket-oneway-shared/websocket-oneway.test.js new file mode 100644 index 000000000..3db39da36 --- /dev/null +++ b/tests/integration/websocket-oneway-shared/websocket-oneway.test.js @@ -0,0 +1,55 @@ +import assert from "node:assert" +import { join } from "desm" +import { WebSocket } from "ws" +import { setup, teardown } from "../../_testHelpers/index.js" +import websocketSend from "../../_testHelpers/websocketPromise.js" +import { BASE_URL } from "../../config.js" + +const { parse, stringify } = JSON + +describe("one way websocket tests on shared port", function desc() { + beforeEach(() => + setup({ + servicePath: join(import.meta.url), + }), + ) + + afterEach(() => teardown()) + + it("websocket echos nothing", async () => { + const url = new URL("/dev", BASE_URL) + url.port = url.port ? "3000" : url.port + url.protocol = "ws" + + const payload = stringify({ + hello: "world", + now: new Date().toISOString(), + }) + + const ws = new WebSocket(url) + const { data, code, err } = await websocketSend(ws, payload) + + assert.equal(code, undefined) + assert.equal(err, undefined) + assert.equal(data, undefined) + }) + + it("execution error emits Internal Server Error", async () => { + const url = new URL("/dev", BASE_URL) + url.port = url.port ? "3000" : url.port + url.protocol = "ws" + + const payload = stringify({ + hello: "world", + now: new Date().toISOString(), + throwError: true, + }) + + const ws = new WebSocket(url) + const { data, code, err } = await websocketSend(ws, payload) + + assert.equal(code, undefined) + assert.equal(err, undefined) + assert.equal(parse(data).message, "Internal server error") + }) +}) diff --git a/tests/integration/websocket-twoway-shared/serverless.yml b/tests/integration/websocket-twoway-shared/serverless.yml new file mode 100644 index 000000000..1f3643c5d --- /dev/null +++ b/tests/integration/websocket-twoway-shared/serverless.yml @@ -0,0 +1,37 @@ +service: twoway-shared-websocket-tests + +configValidationMode: error +deprecationNotificationMode: error + +plugins: + - ../../../src/index.js + +provider: + architecture: arm64 + deploymentMethod: direct + memorySize: 1024 + name: aws + region: us-east-1 + runtime: nodejs18.x + stage: dev + versionFunctions: false + +functions: + handler: + events: + - http: + method: get + path: echo + - websocket: + route: $connect + - websocket: + route: $disconnect + - websocket: + route: $default + # Enable 2-way comms + routeResponseSelectionExpression: $default + handler: src/handler.handler + +custom: + serverless-offline: + websocketPort: 3000 diff --git a/tests/integration/websocket-twoway-shared/src/handler.js b/tests/integration/websocket-twoway-shared/src/handler.js new file mode 100644 index 000000000..55dbdb570 --- /dev/null +++ b/tests/integration/websocket-twoway-shared/src/handler.js @@ -0,0 +1,32 @@ +const { parse } = JSON + +export async function handler(event) { + const { body, queryStringParameters, requestContext } = event + const statusCode = + queryStringParameters && queryStringParameters.statusCode + ? Number(queryStringParameters.statusCode) + : 200 + + if ( + queryStringParameters && + queryStringParameters.throwError && + requestContext && + requestContext.routeKey === "$connect" + ) { + throw new Error("Throwing error during connect phase") + } + + if ( + body && + parse(body).throwError && + requestContext && + requestContext.routeKey === "$default" + ) { + throw new Error("Throwing error from incoming message") + } + + return { + body: body || undefined, + statusCode, + } +} diff --git a/tests/integration/websocket-twoway-shared/src/package.json b/tests/integration/websocket-twoway-shared/src/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/integration/websocket-twoway-shared/src/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/integration/websocket-twoway-shared/websocket-twoway.test.js b/tests/integration/websocket-twoway-shared/websocket-twoway.test.js new file mode 100644 index 000000000..bff283f3a --- /dev/null +++ b/tests/integration/websocket-twoway-shared/websocket-twoway.test.js @@ -0,0 +1,102 @@ +import assert from "node:assert" +import { join } from "desm" +import { WebSocket } from "ws" +import { setup, teardown } from "../../_testHelpers/index.js" +import websocketSend from "../../_testHelpers/websocketPromise.js" +import { BASE_URL } from "../../config.js" + +const { parse, stringify } = JSON + +describe("two way websocket tests on shared port", function desc() { + beforeEach(() => + setup({ + servicePath: join(import.meta.url), + }), + ) + + afterEach(() => teardown()) + + it("websocket echos sent message", async () => { + const url = new URL("/dev", BASE_URL) + url.port = url.port ? "3000" : url.port + url.protocol = "ws" + + const payload = stringify({ + hello: "world", + now: new Date().toISOString(), + }) + + const ws = new WebSocket(url) + const { code, data, err } = await websocketSend(ws, payload) + + assert.equal(code, undefined) + assert.equal(err, undefined) + assert.deepEqual(data, payload) + }) + + // + ;[401, 500, 501, 502].forEach((statusCode) => { + it(`websocket connection emits status code ${statusCode}`, async () => { + const url = new URL("/dev", BASE_URL) + url.port = url.port ? "3000" : url.port + url.searchParams.set("statusCode", statusCode) + url.protocol = "ws" + + const payload = stringify({ + hello: "world", + now: new Date().toISOString(), + }) + + const ws = new WebSocket(url) + const { code, data, err } = await websocketSend(ws, payload) + + assert.equal(code, undefined) + + if (statusCode >= 200 && statusCode < 300) { + assert.equal(err, undefined) + assert.deepEqual(data, payload) + } else { + assert.equal(err.message, `Unexpected server response: ${statusCode}`) + assert.equal(data, undefined) + } + }) + }) + + it("websocket emits 502 on connection error", async () => { + const url = new URL("/dev", BASE_URL) + url.port = url.port ? "3000" : url.port + url.searchParams.set("throwError", "true") + url.protocol = "ws" + + const payload = stringify({ + hello: "world", + now: new Date().toISOString(), + }) + + const ws = new WebSocket(url) + const { code, data, err } = await websocketSend(ws, payload) + + assert.equal(code, undefined) + assert.equal(err.message, "Unexpected server response: 502") + assert.equal(data, undefined) + }) + + it("execution error emits Internal Server Error", async () => { + const url = new URL("/dev", BASE_URL) + url.port = url.port ? "3000" : url.port + url.protocol = "ws" + + const payload = stringify({ + hello: "world", + now: new Date().toISOString(), + throwError: true, + }) + + const ws = new WebSocket(url) + const { code, data, err } = await websocketSend(ws, payload) + + assert.equal(code, undefined) + assert.equal(err, undefined) + assert.equal(parse(data).message, "Internal server error") + }) +}) From 014c48eb0d3c84d43602cceb91874682c52a5dcc Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Tue, 18 Jun 2024 09:03:56 -0400 Subject: [PATCH 6/7] update readme --- README.md | 21 +++++++++++++++---- ...est.js => websocket-oneway-shared.test.js} | 0 ...est.js => websocket-twoway-shared.test.js} | 0 3 files changed, 17 insertions(+), 4 deletions(-) rename tests/integration/websocket-oneway-shared/{websocket-oneway.test.js => websocket-oneway-shared.test.js} (100%) rename tests/integration/websocket-twoway-shared/{websocket-twoway.test.js => websocket-twoway-shared.test.js} (100%) diff --git a/README.md b/README.md index 2cbef13e8..ab376799b 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,14 @@ Default: 600 (10 minutes) WebSocket port to listen on.
Default: 3001 +The `websocketPort` may also be set to the same as `port` so that a single port can be used for mulitple protocols. + +In the event the ports are the same: + +- The [Connections API Server](#websocket-connections-api) will be hosted on `lambdaPort` + +### CLI Options in `serverless.yml` + Any of the CLI options can be added to your `serverless.yml`. For example: ```yml @@ -722,20 +730,25 @@ Example response velocity template: }, ``` -## WebSocket +## WebSocket Connections API -Usage in order to send messages back to clients: +The `connections-port` for the connections API is available at the following endpoint: -`POST http://localhost:3001/@connections/{connectionId}` +- if `websocketPort == 3001`: (connections API and websocket share `websocketPort`) + - `POST http://localhost:3001/@connections/{connectionId}` +- if `websocketPort == port`: (connections API is bound to the `lambdaPort`) + - `POST http://localhost:3002/@connections/{connectionId}` Or, ```js import aws from 'aws-sdk' +const connectionsPort = 3001; // Or 3002 if websocketPort === port in serverless offline options + const apiGatewayManagementApi = new aws.ApiGatewayManagementApi({ apiVersion: '2018-11-29', - endpoint: 'http://localhost:3001', + endpoint: `http://localhost:${connectionsPort}`, }); apiGatewayManagementApi.postToConnection({ diff --git a/tests/integration/websocket-oneway-shared/websocket-oneway.test.js b/tests/integration/websocket-oneway-shared/websocket-oneway-shared.test.js similarity index 100% rename from tests/integration/websocket-oneway-shared/websocket-oneway.test.js rename to tests/integration/websocket-oneway-shared/websocket-oneway-shared.test.js diff --git a/tests/integration/websocket-twoway-shared/websocket-twoway.test.js b/tests/integration/websocket-twoway-shared/websocket-twoway-shared.test.js similarity index 100% rename from tests/integration/websocket-twoway-shared/websocket-twoway.test.js rename to tests/integration/websocket-twoway-shared/websocket-twoway-shared.test.js From 9542f03349121329db40f3468fbdcaac4d737ed8 Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Tue, 18 Jun 2024 09:09:01 -0400 Subject: [PATCH 7/7] update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab376799b..d767f2c15 100644 --- a/README.md +++ b/README.md @@ -245,9 +245,9 @@ Default: 3001 The `websocketPort` may also be set to the same as `port` so that a single port can be used for mulitple protocols. -In the event the ports are the same: +Note: In the event the ports are the same: -- The [Connections API Server](#websocket-connections-api) will be hosted on `lambdaPort` +- The [Connections API Server](#websocket-connections-api) will be hosted on `lambdaPort`. ### CLI Options in `serverless.yml`