UNPKG

6.66 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Bridge = void 0;
4const http_1 = require("http");
5/**
6 * If the `http.Server` handler function throws an error asynchronously,
7 * then it ends up being an unhandled rejection which doesn't kill the node
8 * process which causes the HTTP request to hang indefinitely. So print the
9 * error here and force the process to exit so that the lambda invocation
10 * returns an Unhandled error quickly.
11 */
12process.on('unhandledRejection', err => {
13 console.error('Unhandled rejection:', err);
14 process.exit(1);
15});
16function normalizeNowProxyEvent(event) {
17 let bodyBuffer;
18 const { method, path, headers, encoding, body } = JSON.parse(event.body);
19 if (body) {
20 if (encoding === 'base64') {
21 bodyBuffer = Buffer.from(body, encoding);
22 }
23 else if (encoding === undefined) {
24 bodyBuffer = Buffer.from(body);
25 }
26 else {
27 throw new Error(`Unsupported encoding: ${encoding}`);
28 }
29 }
30 else {
31 bodyBuffer = Buffer.alloc(0);
32 }
33 return { isApiGateway: false, method, path, headers, body: bodyBuffer };
34}
35function normalizeAPIGatewayProxyEvent(event) {
36 let bodyBuffer;
37 const { httpMethod: method, path, headers, body } = event;
38 if (body) {
39 if (event.isBase64Encoded) {
40 bodyBuffer = Buffer.from(body, 'base64');
41 }
42 else {
43 bodyBuffer = Buffer.from(body);
44 }
45 }
46 else {
47 bodyBuffer = Buffer.alloc(0);
48 }
49 return { isApiGateway: true, method, path, headers, body: bodyBuffer };
50}
51function normalizeEvent(event) {
52 if ('Action' in event) {
53 if (event.Action === 'Invoke') {
54 return normalizeNowProxyEvent(event);
55 }
56 else {
57 throw new Error(`Unexpected event.Action: ${event.Action}`);
58 }
59 }
60 else {
61 return normalizeAPIGatewayProxyEvent(event);
62 }
63}
64class Bridge {
65 constructor(server, shouldStoreEvents = false) {
66 this.events = {};
67 this.reqIdSeed = 1;
68 this.shouldStoreEvents = false;
69 this.server = null;
70 this.shouldStoreEvents = shouldStoreEvents;
71 if (server) {
72 this.setServer(server);
73 }
74 this.launcher = this.launcher.bind(this);
75 // This is just to appease TypeScript strict mode, since it doesn't
76 // understand that the Promise constructor is synchronous
77 this.resolveListening = (_info) => { }; // eslint-disable-line @typescript-eslint/no-unused-vars
78 this.listening = new Promise(resolve => {
79 this.resolveListening = resolve;
80 });
81 }
82 setServer(server) {
83 this.server = server;
84 }
85 listen() {
86 const { server, resolveListening } = this;
87 if (!server) {
88 throw new Error('Server has not been set!');
89 }
90 if (typeof server.timeout === 'number' && server.timeout > 0) {
91 // Disable timeout (usually 2 minutes until Node 13).
92 // Instead, user should assign function `maxDuration`.
93 server.timeout = 0;
94 }
95 return server.listen({
96 host: '127.0.0.1',
97 port: 0,
98 }, function listeningCallback() {
99 if (!this || typeof this.address !== 'function') {
100 throw new Error('Missing server.address() function on `this` in server.listen()');
101 }
102 const addr = this.address();
103 if (!addr) {
104 throw new Error('`server.address()` returned `null`');
105 }
106 if (typeof addr === 'string') {
107 throw new Error(`Unexpected string for \`server.address()\`: ${addr}`);
108 }
109 resolveListening(addr);
110 });
111 }
112 async launcher(event, context) {
113 context.callbackWaitsForEmptyEventLoop = false;
114 const { port } = await this.listening;
115 const normalizedEvent = normalizeEvent(event);
116 const { isApiGateway, method, path, headers, body } = normalizedEvent;
117 if (this.shouldStoreEvents) {
118 const reqId = `${this.reqIdSeed++}`;
119 this.events[reqId] = normalizedEvent;
120 headers['x-now-bridge-request-id'] = reqId;
121 }
122 // eslint-disable-next-line consistent-return
123 return new Promise((resolve, reject) => {
124 const opts = { hostname: '127.0.0.1', port, path, method };
125 const req = http_1.request(opts, res => {
126 const response = res;
127 const respBodyChunks = [];
128 response.on('data', chunk => respBodyChunks.push(Buffer.from(chunk)));
129 response.on('error', reject);
130 response.on('end', () => {
131 const bodyBuffer = Buffer.concat(respBodyChunks);
132 delete response.headers.connection;
133 if (isApiGateway) {
134 delete response.headers['content-length'];
135 }
136 else if (response.headers['content-length']) {
137 response.headers['content-length'] = String(bodyBuffer.length);
138 }
139 resolve({
140 statusCode: response.statusCode || 200,
141 headers: response.headers,
142 body: bodyBuffer.toString('base64'),
143 encoding: 'base64',
144 });
145 });
146 });
147 req.on('error', error => {
148 setTimeout(() => {
149 // this lets express print the true error of why the connection was closed.
150 // it is probably 'Cannot set headers after they are sent to the client'
151 reject(error);
152 }, 2);
153 });
154 for (const [name, value] of Object.entries(headers)) {
155 if (value === undefined) {
156 console.error('Skipping HTTP request header %j because value is undefined', name);
157 continue;
158 }
159 try {
160 req.setHeader(name, value);
161 }
162 catch (err) {
163 console.error('Skipping HTTP request header: %j', `${name}: ${value}`);
164 console.error(err.message);
165 }
166 }
167 if (body)
168 req.write(body);
169 req.end();
170 });
171 }
172 consumeEvent(reqId) {
173 const event = this.events[reqId];
174 delete this.events[reqId];
175 return event;
176 }
177}
178exports.Bridge = Bridge;