netlify-lambda
Version:
Build and serve lambda function with webpack compilation
216 lines (193 loc) • 6.73 kB
JavaScript
var express = require('express');
var bodyParser = require('body-parser');
var expressLogging = require('express-logging');
var queryString = require('querystring');
var path = require('path');
var conf = require('./config');
var jwtDecode = require('jwt-decode');
function handleErr(err, response) {
response.statusCode = 500;
response.write('Function invocation failed: ' + err.toString());
response.end();
console.log('Error during invocation: ', err);
return;
}
function handleInvocationTimeout(response, timeout) {
response.statusCode = 500;
response.write(`Function invocation took longer than ${timeout} seconds.`);
response.end();
console.log(
`Your lambda function took longer than ${timeout} seconds to finish.
If you need a longer execution time, you can increase the timeout using the -t or --timeout flag.
Please note that default function invocation is 10 seconds, check our documentation for more information (https://www.netlify.com/docs/functions/#custom-deployment-options).
`,
);
}
function createCallback(response) {
return function callback(err, lambdaResponse) {
if (err) {
return handleErr(err, response);
}
response.statusCode = lambdaResponse.statusCode;
for (const key in lambdaResponse.headers) {
response.setHeader(key, lambdaResponse.headers[key]);
}
for (const key in lambdaResponse.multiValueHeaders) {
const items = lambdaResponse.multiValueHeaders[key];
response.setHeader(key, items);
}
if (lambdaResponse.body) {
response.write(
lambdaResponse.isBase64Encoded
? Buffer.from(lambdaResponse.body, 'base64')
: lambdaResponse.body,
);
} else {
if (
response.statusCode !== 204 &&
process.env.CONTEXT !== 'production' &&
!process.env.SILENCE_EMPTY_LAMBDA_WARNING
)
console.log(
`Your lambda function didn't return a body, which may be a mistake. Check our Usage docs for examples (https://github.com/netlify/netlify-lambda#usage).
If this is intentional, you can silence this warning by setting process.env.SILENCE_EMPTY_LAMBDA_WARNING to a truthy value or process.env.CONTEXT to 'production'`,
);
}
response.end();
};
}
function promiseCallback(promise, callback) {
if (!promise) return;
if (typeof promise.then !== 'function') return;
if (typeof callback !== 'function') return;
return promise.then(
function (data) {
callback(null, data);
},
function (err) {
callback(err, null);
},
);
}
function buildClientContext(headers) {
// inject a client context based on auth header https://github.com/netlify/netlify-lambda/pull/57
if (!headers['authorization']) return;
const parts = headers['authorization'].split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') return;
try {
return {
identity: {
url: 'NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_URL',
token: 'NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_TOKEN',
},
user: jwtDecode(parts[1]),
};
} catch (e) {
return; // Ignore errors - bearer token is not a JWT, probably not intended for us
}
}
function createHandler(dir, isStatic, timeout) {
return function (request, response) {
// handle proxies without path re-writes (http-servr)
var cleanPath = request.path.replace(/^\/.netlify\/functions/, '');
var func = cleanPath.split('/').filter((e) => !!e)[0];
if (typeof func === 'undefined') {
console.error(
`Something went wrong and the function path derived from ${cleanPath} (raw form: ${request.path}) was undefined. Please doublecheck your function naming and toml configuration.`,
);
}
if (typeof dir === 'undefined') {
console.error(
`Something went wrong and the function directory ${dir} was undefined. Please doublecheck your toml configuration.`,
);
}
var module = path.join(process.cwd(), dir, func);
if (isStatic) {
delete require.cache[require.resolve(module)];
}
var handler;
try {
handler = require(module);
} catch (err) {
handleErr(err, response);
return;
}
var isBase64 =
request.body &&
!(request.headers['content-type'] || '').match(
/text|application|multipart\/form-data/,
);
var lambdaRequest = {
path: request.path,
httpMethod: request.method,
queryStringParameters: queryString.parse(request.url.split(/\?(.+)/)[1]),
headers: request.headers,
body: isBase64
? Buffer.from(request.body.toString(), 'utf8').toString('base64')
: request.body,
isBase64Encoded: isBase64,
};
var callback = createCallback(response);
var promise = handler.handler(
lambdaRequest,
{ clientContext: buildClientContext(request.headers) || {} },
callback,
);
var invocationTimeoutRef = null;
Promise.race([
promiseCallback(promise, callback),
new Promise(function (resolve) {
invocationTimeoutRef = setTimeout(function () {
handleInvocationTimeout(response, timeout);
resolve();
}, timeout * 1000);
}),
]).then(
(result) => {
clearTimeout(invocationTimeoutRef);
return result; // not used, but writing this to avoid future footguns
},
(err) => {
clearTimeout(invocationTimeoutRef);
throw err;
},
);
};
}
exports.listen = async function (port, isStatic, timeout) {
var config = await conf.load();
var app = express();
var dir = config.build.functions || config.build.Functions;
app.use(bodyParser.raw({ limit: '6mb' }));
app.use(bodyParser.text({ limit: '6mb', type: '*/*' }));
app.use(
expressLogging(console, {
blacklist: ['/favicon.ico'],
}),
);
app.get('/favicon.ico', function (req, res) {
res.status(204).end();
});
app.get('/', function (req, res) {
res
.status(404)
.send(
`You have requested the root of http://localhost:${port}. This is likely a mistake. netlify-lambda serves functions at http://localhost:${port}/.netlify/functions/your-function-name; please fix your code.`,
);
});
app.all('*', createHandler(dir, isStatic, timeout));
const server = app.listen(port, function (err) {
if (err) {
console.error('Unable to start lambda server: ', err);
process.exit(1);
}
console.log(`Lambda server is listening on ${port}`);
});
return {
clearCache: function (chunk) {
var module = path.join(process.cwd(), dir, chunk);
delete require.cache[require.resolve(module)];
},
stopServer: () => server.close(),
};
};