UNPKG

@benie/lambda-lib

Version:

Builders and tools for creating AWS Lambda function handlers that provides automation for things such as logging, instrumentation and parameters propagation

160 lines (140 loc) 6.56 kB
const aws4 = require('aws4'); const Url = require('url'); const correlationIds = require('../correlation-ids'); const log = require('../log'); const {ClientException, ServerException} = require('./remote-exceptions'); const _libs = { 'http:' : require('http'), 'https:': require('https') }; /** * Makes an HTTP request to a remote API * * @param verb An HTTP verb (i.e., 'GET', 'POST', 'PUT', 'PATCH', ...) * @param url The complete resource URL (i.e., 'https://my.host/a/path') * @param body The request body * @param headers Headers to be included in the request. Some extra headers may be added by this module as needed * @param signAws If the request should be signed with AWS credential from the executing role. Set to true if you are calling another lambda API. * * @returns The parsed response body, if response code is 2XX content-type=application/json; or the raw body otherwise * @throws {ClientException} If response status code is 4XX. The status code and message will be in the thrown exception. * @throws {ServerException} If response status code is 5XX. The status code and message will be in the thrown exception. */ exports.request = _request; /** * Makes an HTTP request to a remote API, signed by the executor AWS role * * @param verb An HTTP verb (i.e., 'GET', 'POST', 'PUT', 'PATCH', ...) * @param url The complete resource URL (i.e., 'https://my.host/a/path') * @param body The request body * @param headers Headers to be included in the request. Some extra headers may be added by this module as needed * * @returns The parsed response body, if response code is 2XX content-type=application/json; or the raw body otherwise * @throws {ClientException} If response status code is 4XX. The status code and message will be in the thrown exception. * @throws {ServerException} If response status code is 5XX. The status code and message will be in the thrown exception. */ exports.awsRequest = async (verb, url, body, headers, region) => {return _request(verb, url, body, headers, true, region);}; async function _request (verb, url, body, headers, signAws, region) { if (! (verb && url) ) { throw new Error ('"verb" and "url" must be informed'); } let bodyAsString; let requestHeaders = Object.assign({}, headers); if (body && body instanceof Object) { bodyAsString = JSON.stringify(body); requestHeaders['content-type'] = requestHeaders['content-type'] || 'application/json'; } else if (body) { bodyAsString = body.toString(); } let response = await _makeRequest(verb, url, (body ? bodyAsString : undefined), requestHeaders, signAws, region); if (response.statusCode >= 400) { let errorCode, message; try { const _body = JSON.parse(response.body); errorCode = _body.errorCode; //errorCode may be passed, on top of http status code for typed error contract message = _body.message; } catch(err) { //not JSON message = response.body; } if(response.statusCode < 500) { throw new ClientException(message, response.statusCode, errorCode, response); } else { throw new ServerException(message, response.statusCode, errorCode, response); } } let contentType = response.headers && (response.headers['content-type'] || response.headers['Content-Type']); if(contentType && contentType.toLowerCase().startsWith('application/json')) { return JSON.parse(response.body); } else { return response.body; } } async function _makeRequest(method, urlString, body, headers, signAws, region) { // create a new Promise return new Promise((resolve, reject) => { /* Node's URL library allows us to create a * URL object from our request string, so we can build * our request for http.get */ let urlStringWithoutDuplicateSlashesFromPath = urlString.replace(/([^:]\/)\/+/g, '$1'); const parsedUrl = Url.parse(encodeURI(urlStringWithoutDuplicateSlashesFromPath)); const requestOptions = _createOptions(method, parsedUrl, body, headers, signAws, region); const lib = _getProperLibraryForProtocol(parsedUrl.protocol); const request = lib.request(requestOptions, res => _onResponse(res, resolve, reject)); /* if there's an error, then reject the Promise * (can be handled with Promise.prototype.catch) */ request.on('error', reject); if(requestOptions.body) { request.write(requestOptions.body); } request.end(); }) .then(result => { log.debug('HTTP response', result); return result; }); } function _getProperLibraryForProtocol(protocol) { const DEFAULT_PROTOCOL = 'https:'; const res = _libs[protocol || DEFAULT_PROTOCOL]; if(!res) { throw new Error(`Could not find handler library for protocol '${protocol}'; must be either 'http:' 'https:'`); } return res; } // the options that are required by http.get function _createOptions(method, url, body, _headers, signAws, region = process.env.AWS_REGION) { let headers = Object.assign({}, correlationIds.get(), _headers); let opts = { hostname: url.hostname, path: url.path, port: url.port, method, body, headers }; log.debug('HTTP request', opts); if (signAws) { opts.region = region; opts.service = 'execute-api'; aws4.sign(opts); log.debug('(HTTP request is IAM-signed)'); } return opts; } /* once http.get returns a response, build it and * resolve or reject the Promise */ function _onResponse(response, resolve, reject) { const hasResponseFailed = response.status >= 400; var responseBody = ''; if (hasResponseFailed) { reject(`Request to ${response.url} failed with HTTP status ${response.status}`); } /* the response stream's (an instance of Stream) current data. See: * https://nodejs.org/api/stream.html#stream_event_data */ response.on('data', chunk => responseBody += chunk.toString()); // once all the data has been read, resolve the Promise response.on('end', () => resolve({statusCode: response.statusCode, body: responseBody, headers: response.headers})); }