├── test ├── binary.js ├── file.txt ├── .eslintrc ├── integration │ ├── image.png │ ├── test.xlsx │ ├── echo.js │ ├── binary.js │ ├── koa.js │ ├── timer.js │ ├── root.js │ ├── pino.js │ ├── compare.js │ ├── express.js │ ├── cookies.js │ ├── test-aws.js │ ├── test-offline.js │ └── test-common.js ├── util │ └── request.js ├── polka.js ├── typecheck.ts ├── fastify.js ├── hapi.js ├── node.js ├── sails.js ├── get-event-type.js ├── loopback4.js ├── inversify.js ├── is-binary.js ├── base-path.js ├── restana.js ├── generic.js ├── express.js ├── format-response.js ├── koa.js ├── clean-up-event.js └── spec.js ├── examples └── hapi │ ├── .gitignore │ ├── src │ ├── routes │ │ └── base.js │ ├── plugins.js │ ├── index.js │ └── server.js │ ├── package.json │ └── serverless.yml ├── .eslintignore ├── .gitignore ├── .editorconfig ├── .eslintrc ├── .npmignore ├── .github └── workflows │ └── ci.yml ├── lib ├── provider │ ├── get-provider.js │ ├── aws │ │ ├── index.js │ │ ├── sanitize-headers.js │ │ ├── get-event-type.js │ │ ├── is-binary.js │ │ ├── format-response.js │ │ ├── clean-up-event.js │ │ └── create-request.js │ └── azure │ │ ├── index.js │ │ ├── sanitize-headers.js │ │ ├── format-response.js │ │ ├── clean-up-request.js │ │ ├── is-binary.js │ │ ├── create-request.js │ │ └── set-cookie.json ├── finish.js ├── request.js ├── framework │ └── get-framework.js └── response.js ├── serverless-http.js ├── docs ├── PROVIDERS.md ├── EXAMPLES.md └── ADVANCED.md ├── serverless-http.d.ts ├── LICENSE.txt ├── serverless.yml ├── package.json └── README.md /test/binary.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/file.txt: -------------------------------------------------------------------------------- 1 | this is a test 2 | -------------------------------------------------------------------------------- /examples/hapi/.gitignore: -------------------------------------------------------------------------------- 1 | .serverless 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | 4 | rules: 5 | require-yield: 0 6 | -------------------------------------------------------------------------------- /test/integration/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougmoscrop/serverless-http/HEAD/test/integration/image.png -------------------------------------------------------------------------------- /test/integration/test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougmoscrop/serverless-http/HEAD/test/integration/test.xlsx -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | *.log 5 | yarn.lock 6 | .vscode 7 | .serverless/ 8 | dist/ 9 | .idea 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # -*- mode: ini-generic -*- 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | env: 4 | node: true 5 | es6: true 6 | 7 | parserOptions: 8 | ecmaVersion: 8 9 | 10 | extends: 11 | "eslint:recommended" 12 | 13 | rules: 14 | no-console: 0 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | coverage/ 3 | docs/ 4 | examples/ 5 | .nyc_output/ 6 | .vscode/ 7 | .serverless/ 8 | .editorconfig 9 | .eslintignore 10 | .eslintrc 11 | .gitignore 12 | .npmignore 13 | .travis.yml 14 | serverless.yml 15 | -------------------------------------------------------------------------------- /examples/hapi/src/routes/base.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = [ 3 | { 4 | method: 'GET', 5 | path: '/', 6 | handler: (request, h) => { 7 | return h.response({ 8 | msg: 'Hello, world!', 9 | serverless: request.serverless 10 | }) 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /test/util/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const serverless = require('../../serverless-http'); 4 | 5 | module.exports = async function(app, request, options) { 6 | if (process.env.INTEGRATION_TEST) { 7 | throw new Error(); 8 | } 9 | 10 | const handler = serverless(app, options); 11 | 12 | return await handler(request); 13 | }; 14 | -------------------------------------------------------------------------------- /test/integration/echo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.handler = async (event, context) => { 4 | const body = JSON.stringify({ event, context }, null, 2); 5 | 6 | console.log(body); 7 | 8 | return { 9 | statusCode: 200, 10 | headers: { 11 | 'content-type': 'application/json' 12 | }, 13 | body, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /test/integration/binary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | 5 | const serverless = require('../../serverless-http'); 6 | 7 | const app = express(); 8 | 9 | app.use(function (req, res) { 10 | res.sendFile('image.png', { root : __dirname}); 11 | }); 12 | 13 | module.exports.handler = serverless(app, { 14 | binary: ['image/*'] 15 | }); 16 | -------------------------------------------------------------------------------- /test/integration/koa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Koa = require('koa'); 4 | 5 | const serverless = require('../../serverless-http'); 6 | 7 | const app = new Koa(); 8 | 9 | app.use(async function (ctx) { 10 | ctx.status = 200; 11 | ctx.body = { 12 | url: ctx.req.url, 13 | method: ctx.req.method.toLowerCase(), 14 | }; 15 | }); 16 | 17 | module.exports.handler = serverless(app); 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | - run: npm ci 18 | - run: npm test 19 | - run: npm run test:integration:offline -------------------------------------------------------------------------------- /lib/provider/get-provider.js: -------------------------------------------------------------------------------- 1 | const aws = require('./aws'); 2 | const azure = require('./azure'); 3 | 4 | const providers = { 5 | aws, 6 | azure 7 | }; 8 | 9 | module.exports = function getProvider(options) { 10 | const { provider = 'aws' } = options; 11 | 12 | if (provider in providers) { 13 | return providers[provider](options); 14 | } 15 | 16 | throw new Error(`Unsupported provider ${provider}`); 17 | }; 18 | -------------------------------------------------------------------------------- /test/integration/timer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | 5 | const serverless = require('../../serverless-http'); 6 | 7 | const app = express(); 8 | 9 | app.use(function (req, res) { 10 | setTimeout(() => { 11 | console.log('taking my time'); 12 | }, 30000) 13 | res.status(200).json({ 14 | url: req.url 15 | }); 16 | }); 17 | 18 | module.exports.handler = serverless(app); 19 | -------------------------------------------------------------------------------- /test/integration/root.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | 5 | const serverless = require('../../serverless-http'); 6 | 7 | const app = express(); 8 | 9 | app.get('/', function (req, res) { 10 | res.status(200).json({ 11 | originalUrl: req.originalUrl, 12 | url: req.url, 13 | method: req.method.toLowerCase(), 14 | }); 15 | }); 16 | 17 | module.exports.handler = serverless(app); 18 | -------------------------------------------------------------------------------- /examples/hapi/src/plugins.js: -------------------------------------------------------------------------------- 1 | 2 | const serverlessPlugin = { 3 | name: 'Serverless Plugin', 4 | version: '1.0.0', 5 | register: async (server) => { 6 | server.ext('onRequest', (request, h) => { 7 | try { 8 | request.serverless = request.raw.req.serverless 9 | } catch (err) { 10 | console.log('err:', err) 11 | request.serverless = {} 12 | } 13 | 14 | return h.continue 15 | }) 16 | } 17 | } 18 | 19 | module.exports = [serverlessPlugin] 20 | -------------------------------------------------------------------------------- /test/integration/pino.js: -------------------------------------------------------------------------------- 1 | const serverless = require('../../serverless-http'); 2 | const express = require('express'); 3 | const pino = require('pino'); 4 | const pinoHttp = require('pino-http'); 5 | 6 | const app = express(); 7 | const basicPinoLogger = pino({level: 'info'}); 8 | const expressPino = pinoHttp({ 9 | logger: basicPinoLogger 10 | }); 11 | 12 | app.use(expressPino); 13 | 14 | app.get('/pino', (req, res) => { 15 | return res.status(200).json({status: 'ok'}); 16 | }); 17 | 18 | exports.handler = serverless(app); -------------------------------------------------------------------------------- /examples/hapi/src/index.js: -------------------------------------------------------------------------------- 1 | 2 | // Modules 3 | const serverless = require('serverless-http') 4 | const { getServer } = require('./server') 5 | 6 | // Cache 7 | let handler 8 | 9 | // Handler 10 | module.exports.handler = async (event, context) => { 11 | if (!handler) { 12 | const app = await getServer() 13 | handler = serverless(app, { 14 | request: (request) => { 15 | request.serverless = { event, context } 16 | } 17 | }) 18 | } 19 | 20 | const res = await handler(event, context) 21 | 22 | return res 23 | } 24 | -------------------------------------------------------------------------------- /lib/provider/aws/index.js: -------------------------------------------------------------------------------- 1 | const cleanUpEvent = require('./clean-up-event'); 2 | 3 | const createRequest = require('./create-request'); 4 | const formatResponse = require('./format-response'); 5 | 6 | module.exports = options => { 7 | return getResponse => async (event_, context = {}) => { 8 | const event = cleanUpEvent(event_, options); 9 | 10 | const request = createRequest(event, context, options); 11 | const response = await getResponse(request, event, context); 12 | 13 | return formatResponse(event, response, options); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/provider/azure/index.js: -------------------------------------------------------------------------------- 1 | const cleanupRequest = require('./clean-up-request'); 2 | const createRequest = require('./create-request'); 3 | const formatResponse = require('./format-response'); 4 | 5 | module.exports = options => { 6 | return getResponse => async (context, req) => { 7 | const event = cleanupRequest(req, options); 8 | const request = createRequest(event, options); 9 | const response = await getResponse(request, context, event); 10 | context.log(response); 11 | return formatResponse(response, options); 12 | } 13 | }; -------------------------------------------------------------------------------- /lib/provider/aws/sanitize-headers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function sanitizeHeaders(headers) { 4 | return Object.keys(headers).reduce((memo, key) => { 5 | const value = headers[key]; 6 | 7 | if (Array.isArray(value)) { 8 | memo.multiValueHeaders[key] = value; 9 | if (key.toLowerCase() !== 'set-cookie') { 10 | memo.headers[key] = value.join(", "); 11 | } 12 | } else { 13 | memo.headers[key] = value == null ? '' : value.toString(); 14 | } 15 | 16 | return memo; 17 | }, { 18 | headers: {}, 19 | multiValueHeaders: {} 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /test/polka.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const polka = require('polka'), 3 | expect = require('chai').expect, 4 | request = require('./util/request'); 5 | 6 | describe('polka', () => { 7 | let app; 8 | 9 | beforeEach(function() { 10 | app = polka(); 11 | }); 12 | 13 | it('basic hello world', () => { 14 | app.get('/', (req, res) => { 15 | res.end('hello world'); 16 | }); 17 | 18 | return request(app, { 19 | httpMethod: 'GET', 20 | path: '/' 21 | }) 22 | .then(res => { 23 | expect(res.statusCode).to.equal(200); 24 | expect(res.body).to.equal('hello world'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/typecheck.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from "koa" 2 | import * as serverlessHttp from ".." 3 | 4 | // Basic definitions check. 5 | const handlerWithFn: serverlessHttp.Handler = serverlessHttp(() => { }) as serverlessHttp.Handler; 6 | const handlerWithApp: serverlessHttp.Handler = serverlessHttp(new Koa()) as serverlessHttp.Handler; 7 | 8 | // Options 9 | const handlerWithDefaultOptions: serverlessHttp.Handler = serverlessHttp(new Koa(), { 10 | }) as serverlessHttp.Handler; 11 | 12 | // Options 13 | const handlerWithRequestId: serverlessHttp.Handler = serverlessHttp(new Koa(), { 14 | requestId: 'x-my-req-id' 15 | }) as serverlessHttp.Handler; 16 | 17 | -------------------------------------------------------------------------------- /examples/hapi/src/server.js: -------------------------------------------------------------------------------- 1 | 2 | // Modules 3 | const hapi = require('hapi') 4 | const plugins = require('./plugins') 5 | 6 | // Config 7 | const port = process.env.APP_PORT || 8080 8 | 9 | // Server 10 | const serverParams = { port } 11 | const server = hapi.server(serverParams) 12 | 13 | // Routes 14 | server.route(require('./routes/base')) 15 | 16 | // Main 17 | const getServer = async () => { 18 | await server.register(plugins) 19 | return server 20 | } 21 | 22 | const listen = async () => { 23 | await server.start() 24 | console.log(`Server running at: ${server.info.uri}`) 25 | } 26 | 27 | // Export 28 | module.exports = { 29 | getServer, 30 | listen 31 | } 32 | -------------------------------------------------------------------------------- /test/fastify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fastify = require('fastify'), 3 | expect = require('chai').expect, 4 | request = require('./util/request'); 5 | 6 | describe('fastify', () => { 7 | let app; 8 | 9 | beforeEach(function() { 10 | app = fastify(); 11 | }); 12 | 13 | it('basic hello world', () => { 14 | app.get('/', (req, res) => { 15 | res.send('hello world'); 16 | }) 17 | 18 | app.ready(); 19 | 20 | return request(app, { 21 | httpMethod: 'GET', 22 | path: '/' 23 | }) 24 | .then(res => { 25 | expect(res.statusCode).to.equal(200); 26 | expect(res.body).to.equal('hello world'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/hapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-http-hapi-example", 3 | "version": "1.0.0", 4 | "description": "How to use serverless-http with hapi.js", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "lint": "npx standard --fix && npx standard", 8 | "test": "npm run lint", 9 | "start": "node ./src", 10 | "predeploy": "npm test", 11 | "deploy": "npx serverless deploy" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "hapi": "^18.0.0", 18 | "serverless-http": "^1.9.0" 19 | }, 20 | "devDependencies": { 21 | "serverless": "^1.36.3", 22 | "standard": "^12.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/hapi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hapi = require('hapi'), 4 | expect = require('chai').expect, 5 | request = require('./util/request'); 6 | 7 | describe('hapi', () => { 8 | let app; 9 | 10 | beforeEach(function() { 11 | app = Hapi.server(); 12 | }); 13 | 14 | it('basic hello world', () => { 15 | app.route({ 16 | method: 'GET', 17 | path: '/', 18 | handler: () => 'Hello, world!' 19 | }); 20 | 21 | return request(app, { 22 | httpMethod: 'GET', 23 | path: '/' 24 | }) 25 | .then(response => { 26 | expect(response.statusCode).to.equal(200); 27 | expect(response.body).to.equal('Hello, world!'); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/provider/azure/sanitize-headers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const setCookieVariations = require('./set-cookie.json').variations; 4 | 5 | module.exports = function sanitizeHeaders(headers) { 6 | return Object.keys(headers).reduce((memo, key) => { 7 | const value = headers[key]; 8 | 9 | if (Array.isArray(value)) { 10 | if (key.toLowerCase() === 'set-cookie') { 11 | value.forEach((cookie, i) => { 12 | memo[setCookieVariations[i]] = cookie; 13 | }); 14 | } else { 15 | memo[key] = value.join(', '); 16 | } 17 | } else { 18 | memo[key] = value == null ? '' : value.toString(); 19 | } 20 | 21 | return memo; 22 | }, {}); 23 | }; 24 | -------------------------------------------------------------------------------- /test/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'), 4 | expect = require('chai').expect, 5 | request = require('./util/request'); 6 | 7 | describe('node http server', () => { 8 | let app; 9 | 10 | it('should set statusCode and default body', () => { 11 | app = http.createServer((req, res) => { 12 | res.writeHead(200, { 'Content-Type': 'text/plain' }) 13 | res.write('Hello, world!') 14 | res.end() 15 | }) 16 | return request(app, { 17 | httpMethod: 'GET', 18 | path: '/' 19 | }) 20 | .then(response => { 21 | expect(response.statusCode).to.equal(200); 22 | expect(response.body).to.equal('Hello, world!'); 23 | }); 24 | }) 25 | }) -------------------------------------------------------------------------------- /test/sails.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sails = require('sails'), 4 | expect = require('chai').expect, 5 | request = require('./util/request'); 6 | 7 | describe('sails', () => { 8 | let app; 9 | 10 | beforeEach(function (done) { 11 | this.timeout(6000); 12 | app = sails.load({ 13 | hooks: { 14 | session: false 15 | } 16 | }, err => { 17 | done(err); 18 | }); 19 | }); 20 | 21 | it('basic unconfigured should set 404 statusCode and default body', () => { 22 | return request(app, { 23 | httpMethod: 'GET', 24 | path: '/' 25 | }) 26 | .then(response => { 27 | expect(response.statusCode).to.equal(404); 28 | expect(response.body).to.equal('Not Found'); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /lib/provider/azure/format-response.js: -------------------------------------------------------------------------------- 1 | const isBinary = require('./is-binary'); 2 | const Response = require('../../response'); 3 | const sanitizeHeaders = require('./sanitize-headers'); 4 | 5 | module.exports = (response, options) => { 6 | const { statusCode } = response; 7 | const headers = sanitizeHeaders(Response.headers(response)); 8 | 9 | if (headers['transfer-encoding'] === 'chunked' || response.chunkedEncoding) { 10 | throw new Error('chunked encoding not supported'); 11 | } 12 | 13 | const isBase64Encoded = isBinary(headers, options); 14 | const encoding = isBase64Encoded ? 'base64' : 'utf8'; 15 | const body = Response.body(response).toString(encoding); 16 | 17 | return { status: statusCode, headers, isBase64Encoded, body }; 18 | } -------------------------------------------------------------------------------- /examples/hapi/serverless.yml: -------------------------------------------------------------------------------- 1 | 2 | service: serverless-http-hapi-example 3 | frameWorkVersion: ">=1.1.0 <2.0.0" 4 | 5 | provider: 6 | name: aws 7 | runtime: nodejs8.10 8 | region: eu-central-1 9 | stage: ${opt:stage, 'dev'} 10 | memorySize: 512 11 | apiKeys: 12 | - ${self:custom.prefix}-admin-panel 13 | environment: 14 | NODE_ENV: ${self:provider.stage} 15 | 16 | custom: 17 | prefix: ${self:provider.stage}-${self:service} 18 | 19 | functions: 20 | api: 21 | handler: src/index.handler 22 | events: 23 | - http: 24 | path: / 25 | method: ANY 26 | cors: true 27 | private: true 28 | - http: 29 | path: /{any+} 30 | method: ANY 31 | cors: true 32 | private: true 33 | -------------------------------------------------------------------------------- /serverless-http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const finish = require('./lib/finish'); 4 | const getFramework = require('./lib/framework/get-framework'); 5 | const getProvider = require('./lib/provider/get-provider'); 6 | 7 | const defaultOptions = { 8 | requestId: 'x-request-id' 9 | }; 10 | 11 | module.exports = function (app, opts) { 12 | const options = Object.assign({}, defaultOptions, opts); 13 | 14 | const framework = getFramework(app); 15 | const provider = getProvider(options); 16 | 17 | return provider(async (request, ...context) => { 18 | await finish(request, options.request, ...context); 19 | const response = await framework(request); 20 | await finish(response, options.response, ...context); 21 | response.emit('close'); 22 | return response; 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /docs/PROVIDERS.md: -------------------------------------------------------------------------------- 1 | # Providers 2 | 3 | ## AWS 4 | 5 | Before using this module, you should already understand API Gateway and AWS Lambda. Specifically, you *must* be using Proxy Integration. 6 | 7 | ### Binary Support 8 | 9 | Starting with v1.5.0, `serverless-http` supports API Gateway binary modes. Binary support will base64 decode the incoming request body - when API Gateway specifies that it is encoded - and will base64 encode a response body if the `Content-Type` or `Content-Encoding` matches a known binary type/encoding. This means you can gzip your JSON or return a raw image, but it requires advanced configuration within API Gateway and is generally not fun to work with (consider yourself warned!) 10 | 11 | Existing serverless-http APIs (i.e. those that return JSON as text) should not be affected. See advanced configuration documentation for details. 12 | -------------------------------------------------------------------------------- /test/get-event-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getEventType, LAMBDA_EVENT_TYPES } = require('../lib/provider/aws/get-event-type'); 4 | const expect = require('chai').expect; 5 | 6 | describe('getEventType', function () { 7 | 8 | it('handles an ALB event', () => { 9 | const event = { requestContext: { elb: { arn: 'foo' } } }; 10 | expect(getEventType(event)).to.equal(LAMBDA_EVENT_TYPES.ALB); 11 | }); 12 | 13 | it('handles an HTTP_API_V2 event', () => { 14 | const event = { version: '2.0' }; 15 | expect(getEventType(event)).to.equal(LAMBDA_EVENT_TYPES.HTTP_API_V2); 16 | }); 17 | 18 | it('handles an HTTP_API_V1 event', () => { 19 | const event = { version: '1.0' }; 20 | expect(getEventType(event)).to.equal(LAMBDA_EVENT_TYPES.HTTP_API_V1); 21 | }); 22 | 23 | 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/integration/compare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | module.exports.handler = async (event) => { 7 | const expected = new Buffer(fs.readFileSync(path.join(__dirname, 'test.xlsx'), { encoding: 'binary' })); 8 | 9 | const { body } = event; 10 | 11 | const actualBinary = Buffer.from(body, 'binary'); 12 | const actualUtf8 = Buffer.from(body, 'utf8'); 13 | 14 | return { 15 | statusCode: 200, 16 | headers: { 17 | 'content-type': 'application/json' 18 | }, 19 | body: JSON.stringify({ 20 | expectedBytes: Buffer.byteLength(expected), 21 | binary: { 22 | actualBytes: Buffer.byteLength(actualBinary), 23 | equal: expected.equals(actualBinary), 24 | }, 25 | utf8: { 26 | actualBytes: Buffer.byteLength(actualUtf8), 27 | equal: expected.equals(actualUtf8), 28 | }, 29 | }), 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/provider/aws/get-event-type.js: -------------------------------------------------------------------------------- 1 | const HTTP_API_V1 = 'HTTP_API_V1'; 2 | const HTTP_API_V2 = 'HTTP_API_V2'; 3 | const ALB = 'ALB'; 4 | 5 | const LAMBDA_EVENT_TYPES = { 6 | HTTP_API_V1, 7 | HTTP_API_V2, 8 | ALB 9 | } 10 | 11 | const getEventType = (event) => { 12 | if (event.requestContext && event.requestContext.elb) { 13 | // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html 14 | return ALB; 15 | } else if (event.version === '2.0') { 16 | // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.v2 17 | return HTTP_API_V2; 18 | } else { 19 | // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.v2 20 | return HTTP_API_V1; 21 | } 22 | } 23 | 24 | module.exports = { 25 | getEventType, 26 | LAMBDA_EVENT_TYPES 27 | } -------------------------------------------------------------------------------- /lib/finish.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async function finish(item, transform, ...details) { 4 | await new Promise((resolve, reject) => { 5 | if (item.finished || item.complete) { 6 | resolve(); 7 | return; 8 | } 9 | 10 | let finished = false; 11 | 12 | function done(err) { 13 | if (finished) { 14 | return; 15 | } 16 | 17 | finished = true; 18 | 19 | item.removeListener('error', done); 20 | item.removeListener('end', done); 21 | item.removeListener('finish', done); 22 | 23 | if (err) { 24 | reject(err); 25 | } else { 26 | resolve(); 27 | } 28 | } 29 | 30 | item.once('error', done); 31 | item.once('end', done); 32 | item.once('finish', done); 33 | }); 34 | 35 | if (typeof transform === 'function') { 36 | await transform(item, ...details); 37 | } else if (typeof transform === 'object' && transform !== null) { 38 | Object.assign(item, transform); 39 | } 40 | 41 | return item; 42 | }; 43 | -------------------------------------------------------------------------------- /test/integration/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | 5 | const serverless = require('../../serverless-http'); 6 | 7 | const app = express(); 8 | 9 | app.get('/express', function (req, res) { 10 | res.status(200).json({ 11 | originalUrl: req.originalUrl, 12 | url: req.url, 13 | method: req.method.toLowerCase(), 14 | }); 15 | }); 16 | 17 | app.post('/express', function (req, res) { 18 | res.status(200).json({ 19 | originalUrl: req.originalUrl, 20 | url: req.url, 21 | method: req.method.toLowerCase(), 22 | }); 23 | }); 24 | 25 | app.put('/express', function (req, res) { 26 | res.status(200).json({ 27 | originalUrl: req.originalUrl, 28 | url: req.url, 29 | method: req.method.toLowerCase(), 30 | }); 31 | }); 32 | 33 | app.get('/express/pathed/:id', function (req, res) { 34 | res.status(200).json({ 35 | originalUrl: req.originalUrl, 36 | url: req.url, 37 | method: req.method.toLowerCase(), 38 | id: req.params.id, 39 | }); 40 | }); 41 | 42 | module.exports.handler = serverless(app); 43 | -------------------------------------------------------------------------------- /test/loopback4.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const loopback = require("@loopback/rest"), 4 | expect = require("chai").expect, 5 | request = require("./util/request"); 6 | 7 | describe("loopback", () => { 8 | let app; 9 | 10 | beforeEach(function() { 11 | 12 | const restApp = new loopback.RestApplication(); 13 | 14 | const spec = { 15 | responses: { 16 | '200': { 17 | description: 'greeting text', 18 | content: { 19 | 'application/json': { 20 | schema: {type: 'string'}, 21 | }, 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | restApp.route("get", "/", spec, function greet() { 28 | return "Hello, world!"; 29 | }); // attaches route to RestServer 30 | 31 | app = restApp.requestHandler; 32 | }); 33 | 34 | it("basic hello world", async () => { 35 | return await request(app, { 36 | httpMethod: "GET", 37 | path: "/" 38 | }).then(response => { 39 | expect(response.statusCode).to.equal(200); 40 | expect(response.body).to.equal("Hello, world!"); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/integration/cookies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express') 4 | const cookieParser = require('cookie-parser') 5 | const lambdaLog = require('lambda-log') 6 | const serverless = require('../../serverless-http'); 7 | 8 | const app = express() 9 | app.use(cookieParser()) 10 | app.get('/cookies', getAppContainerHtml) 11 | 12 | module.exports.handler = serverless(app) 13 | 14 | function getAppContainerHtml (req, res) { 15 | lambdaLog.options.debug = true 16 | lambdaLog.options.meta.cookies = req.cookies 17 | lambdaLog.debug('Retrieving appContainerPlaceholderHtml') 18 | 19 | const appContainerPlaceholderHTML = 'Some random string' 20 | 21 | if (appContainerPlaceholderHTML && appContainerPlaceholderHTML.length) { 22 | lambdaLog.debug('Successfully retrieved appContainerPlaceholderHtml', { 23 | appContainerPlaceholderHtml: appContainerPlaceholderHTML 24 | }) 25 | } else { 26 | lambdaLog.warn('Received empty string instead of appContainerPlaceholder', { 27 | appContainerPlaceholderHtml: appContainerPlaceholderHTML 28 | }) 29 | } 30 | 31 | return res.status(200).send(appContainerPlaceholderHTML) 32 | } 33 | -------------------------------------------------------------------------------- /test/integration/test-aws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-console */ 4 | 5 | const { URL } = require('url'); 6 | const { execSync } = require('child_process') 7 | const common = require('./test-common'); 8 | 9 | function run(cmd) { 10 | const res = execSync(`npx serverless ${cmd}`) 11 | return res.toString() 12 | } 13 | 14 | function getEndpoints(info) { 15 | return info.reduce((memo, msg) => { 16 | if (msg.indexOf('endpoints') !== -1) { 17 | msg.split('\n').slice(1).map(txt => { 18 | if (txt) { 19 | const endpoint = txt.replace(' ANY - ', ''); 20 | const url = new URL(endpoint); 21 | memo.push(url); 22 | } 23 | }); 24 | } 25 | return memo; 26 | }, []); 27 | } 28 | 29 | common.runtimes.forEach(runtime => { 30 | describe(runtime, function () { 31 | this.slow(5000); 32 | this.timeout(10000); 33 | 34 | before(function() { 35 | this.timeout(0); 36 | process.env.RUNTIME = runtime 37 | run('deploy'); 38 | }); 39 | 40 | before(async function() { 41 | this.timeout(10000); 42 | const info = run('info'); 43 | this.endpoints = await getEndpoints(info.split('\r?\n')); 44 | }); 45 | 46 | common.shouldBehaveLikeIntegration(); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /serverless-http.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "http"; 2 | 3 | declare namespace ServerlessHttp { 4 | export interface FrameworkApplication { 5 | callback: Function; 6 | handle: Function; 7 | router: { 8 | route: Function; 9 | } 10 | _core: { 11 | _dispatch: Function; 12 | } 13 | } 14 | 15 | /** 16 | * Handler-compatible function, application or plain http server. 17 | */ 18 | export type Application = Function | Partial | Server; 19 | export type Result = Function | Partial | Server; 20 | 21 | export type Options = { 22 | provider?: 'aws' | 'azure' 23 | requestId?: string, 24 | request?: Object | Function, 25 | response?: Object | Function, 26 | binary?: boolean | Function | string | string[], 27 | basePath?: string 28 | } 29 | /** 30 | * AWS Lambda APIGatewayProxyHandler-like handler. 31 | */ 32 | export type Handler = ( 33 | event: Object, 34 | context: Object 35 | ) => Promise; 36 | } 37 | 38 | /** 39 | * Wraps the application into a Lambda APIGatewayProxyHandler-like handler. 40 | */ 41 | declare function ServerlessHttp(application: ServerlessHttp.Application, options?: ServerlessHttp.Options): ServerlessHttp.Handler; 42 | 43 | export = ServerlessHttp; 44 | -------------------------------------------------------------------------------- /lib/provider/azure/clean-up-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function getUrl({ requestPath, url }) { 4 | if (requestPath) { 5 | return requestPath; 6 | } 7 | 8 | return typeof url === 'string' ? url : '/'; 9 | } 10 | 11 | function getRequestContext(request) { 12 | const requestContext = {}; 13 | requestContext.identity = {}; 14 | const forwardedIp = request.headers['x-forwarded-for']; 15 | const clientIp = request.headers['client-ip']; 16 | const ip = forwardedIp ? forwardedIp : (clientIp ? clientIp : ''); 17 | if (ip) { 18 | requestContext.identity.sourceIp = ip.split(':')[0]; 19 | } 20 | return requestContext; 21 | } 22 | 23 | module.exports = function cleanupRequest(req, options) { 24 | const request = req || {}; 25 | 26 | request.requestContext = getRequestContext(req); 27 | request.method = request.method || 'GET'; 28 | request.url = getUrl(request); 29 | request.body = request.body || ''; 30 | request.headers = request.headers || {}; 31 | 32 | if (options.basePath) { 33 | const basePathIndex = request.url.indexOf(options.basePath); 34 | 35 | if (basePathIndex > -1) { 36 | request.url = request.url.substr(basePathIndex + options.basePath.length); 37 | } 38 | } 39 | 40 | return request; 41 | } -------------------------------------------------------------------------------- /lib/provider/aws/is-binary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BINARY_ENCODINGS = ['gzip', 'deflate', 'br']; 4 | const BINARY_CONTENT_TYPES = (process.env.BINARY_CONTENT_TYPES || '').split(','); 5 | 6 | function isBinaryEncoding(headers) { 7 | const contentEncoding = headers['content-encoding']; 8 | 9 | if (typeof contentEncoding === 'string') { 10 | return contentEncoding.split(',').some(value => 11 | BINARY_ENCODINGS.some(binaryEncoding => value.indexOf(binaryEncoding) !== -1) 12 | ); 13 | } 14 | } 15 | 16 | function isBinaryContent(headers, options) { 17 | const contentTypes = [].concat(options.binary 18 | ? options.binary 19 | : BINARY_CONTENT_TYPES 20 | ).map(candidate => 21 | new RegExp(`^${candidate.replace(/\*/g, '.*')}$`) 22 | ); 23 | 24 | const contentType = (headers['content-type'] || '').split(';')[0]; 25 | return !!contentType && contentTypes.some(candidate => candidate.test(contentType)); 26 | } 27 | 28 | module.exports = function isBinary(headers, options) { 29 | if (options.binary === false) { 30 | return false; 31 | } 32 | 33 | if (options.binary === true) { 34 | return true 35 | } 36 | 37 | if (typeof options.binary === 'function') { 38 | return options.binary(headers); 39 | } 40 | 41 | return isBinaryEncoding(headers) || isBinaryContent(headers, options); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/provider/azure/is-binary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BINARY_ENCODINGS = ['gzip', 'deflate', 'br']; 4 | const BINARY_CONTENT_TYPES = (process.env.BINARY_CONTENT_TYPES || '').split(','); 5 | 6 | function isBinaryEncoding(headers) { 7 | const contentEncoding = headers['content-encoding']; 8 | 9 | if (typeof contentEncoding === 'string') { 10 | return contentEncoding.split(',').some(value => 11 | BINARY_ENCODINGS.some(binaryEncoding => value.indexOf(binaryEncoding) !== -1) 12 | ); 13 | } 14 | } 15 | 16 | function isBinaryContent(headers, options) { 17 | const contentTypes = [].concat(options.binary 18 | ? options.binary 19 | : BINARY_CONTENT_TYPES 20 | ).map(candidate => 21 | new RegExp(`^${candidate.replace(/\*/g, '.*')}$`) 22 | ); 23 | 24 | const contentType = (headers['content-type'] || '').split(';')[0]; 25 | return !!contentType && contentTypes.some(candidate => candidate.test(contentType)); 26 | } 27 | 28 | module.exports = function isBinary(headers, options) { 29 | if (options.binary === false) { 30 | return false; 31 | } 32 | 33 | if (options.binary === true) { 34 | return true 35 | } 36 | 37 | if (typeof options.binary === 'function') { 38 | return options.binary(headers); 39 | } 40 | 41 | return isBinaryEncoding(headers) || isBinaryContent(headers, options); 42 | }; 43 | -------------------------------------------------------------------------------- /test/inversify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('reflect-metadata'); 4 | 5 | const bodyParser = require('body-parser'), 6 | expect = require('chai').expect, 7 | request = require('./util/request'), 8 | { Container } = require('inversify'), 9 | { InversifyExpressServer } = require('inversify-express-utils'); 10 | 11 | describe('inversify', () => { 12 | let app; 13 | 14 | beforeEach(function() { 15 | const container = new Container(); 16 | const server = new InversifyExpressServer(container, null, null, null, null, false); 17 | 18 | server.setConfig((app) => { 19 | // add body parser 20 | app.use(bodyParser.urlencoded({ 21 | extended: true 22 | })); 23 | app.use(bodyParser.json()); 24 | app.use((req, res) => { 25 | res.json({ test: req.body.test }); 26 | }); 27 | }); 28 | 29 | app = server.build(); 30 | }); 31 | 32 | it('basic request with body', () => { 33 | return request(app, { 34 | httpMethod: 'POST', 35 | path: '/', 36 | headers: { 37 | 'content-type': 'application/json' 38 | }, 39 | body: { 40 | test: 'testing' 41 | } 42 | }) 43 | .then(response => { 44 | expect(response.statusCode).to.equal(200); 45 | expect(response.body).to.equal('{"test":"testing"}'); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /lib/provider/azure/create-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const url = require('url'); 4 | 5 | const Request = require('../../request'); 6 | 7 | function requestHeaders(request) { 8 | return Object.keys(request.headers).reduce((headers, key) => { 9 | headers[key.toLowerCase()] = request.headers[key]; 10 | return headers; 11 | }, {}); 12 | } 13 | 14 | function requestBody(request) { 15 | const type = typeof request.rawBody; 16 | 17 | if (Buffer.isBuffer(request.rawBody)) { 18 | return request.rawBody; 19 | } else if (type === 'string') { 20 | return Buffer.from(request.rawBody, 'utf8'); 21 | } else if (type === 'object') { 22 | return Buffer.from(JSON.stringify(request.rawBody)); 23 | } 24 | 25 | throw new Error(`Unexpected request.body type: ${typeof request.rawBody}`); 26 | } 27 | 28 | module.exports = (request) => { 29 | const method = request.method; 30 | const query = request.query; 31 | const headers = requestHeaders(request); 32 | const body = requestBody(request); 33 | 34 | const req = new Request({ 35 | method, 36 | headers, 37 | body, 38 | url: url.format({ 39 | pathname: request.url, 40 | query 41 | }) 42 | }); 43 | req.requestContext = request.requestContext; 44 | return req; 45 | } 46 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const { PassThrough } = require('stream'); 5 | 6 | module.exports = class ServerlessRequest extends http.IncomingMessage { 7 | constructor({ method, url, headers, body, remoteAddress }) { 8 | // Create a real readable socket for IncomingMessage instead of a stub. 9 | const socket = new PassThrough(); 10 | socket.encrypted = true; 11 | socket.remoteAddress = remoteAddress; 12 | socket.address = () => ({ port: 443 }); 13 | 14 | super(socket); 15 | 16 | if (typeof headers['content-length'] === 'undefined') { 17 | headers['content-length'] = Buffer.byteLength(body); 18 | } 19 | 20 | Object.assign(this, { 21 | ip: remoteAddress, 22 | complete: true, 23 | httpVersion: '1.1', 24 | httpVersionMajor: '1', 25 | httpVersionMinor: '1', 26 | method, 27 | headers, 28 | body, 29 | url, 30 | }); 31 | 32 | // Make the request stream readable by pushing the body when consumed. 33 | this._read = () => { 34 | if (typeof body !== 'undefined' && body !== null) { 35 | this.push(body); 36 | } 37 | this.push(null); 38 | }; 39 | 40 | // If there's no body, emit 'end' on next tick so on-finished(req) fires 41 | // even when no one consumes the stream. 42 | if (!body || Buffer.byteLength(body) === 0) { 43 | setImmediate(() => this.emit('end')); 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Doug Moscrop 4 | 5 | The following license applies to all parts of this software except as 6 | documented below: 7 | 8 | ==== 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | ==== 29 | 30 | All files located in the node_modules and external directories are 31 | externally maintained libraries used by this software which have their 32 | own licenses; we recommend you read them, as their terms may differ from 33 | the terms above. 34 | -------------------------------------------------------------------------------- /test/is-binary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isBinary = require('../lib/provider/aws/is-binary'); 4 | const expect = require('chai').expect; 5 | const sinon = require('sinon'); 6 | 7 | describe('is-binary', function() { 8 | 9 | it('handles charset', function() { 10 | const result = isBinary({ ['content-type']: 'application/json; charset:utf-8' }, { 11 | binary: ['application/json'] 12 | }); 13 | 14 | expect(result).to.be.true; 15 | }); 16 | 17 | it('handles wildcard', function() { 18 | const result = isBinary({ ['content-type']: 'image/png' }, { 19 | binary: ['image/*'] 20 | }); 21 | 22 | expect(result).to.be.true; 23 | }); 24 | 25 | it('handles double wildcard', function() { 26 | const result = isBinary({ ['content-type']: 'application/json' }, { 27 | binary: ['*/*'] 28 | }); 29 | 30 | expect(result).to.be.true; 31 | }); 32 | 33 | it('does not incorrectly handle wildcards', function() { 34 | const result = isBinary({ ['content-type']: 'application/json' }, { 35 | binary: ['image/*'] 36 | }); 37 | 38 | expect(result).to.be.false; 39 | }); 40 | 41 | it('handles ; separater', function() { 42 | const result = isBinary({ ['content-type']: 'application/json; foo=bar' }, { 43 | binary: ['*/*'] 44 | }); 45 | 46 | expect(result).to.be.true; 47 | }); 48 | 49 | it('force to false', function() { 50 | const result = isBinary({}, { 51 | binary: false 52 | }); 53 | 54 | expect(result).to.be.false; 55 | }); 56 | 57 | it('custom function', function() { 58 | const stub = sinon.stub().returns(true); 59 | const result = isBinary({}, { 60 | binary: stub 61 | }); 62 | 63 | expect(result).to.be.true; 64 | expect(stub.calledOnce).to.be.true; 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/integration/test-offline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-console */ 4 | 5 | const common = require('./test-common'); 6 | const { URL } = require('url'); 7 | const { spawn } = require('child_process') 8 | const kill = require('tree-kill'); 9 | 10 | function sleep(ms) { 11 | return new Promise((resolve) => setInterval(resolve, ms)); 12 | } 13 | 14 | async function runSpawned(cmd) { 15 | const subprocess = spawn("npx", ["serverless", cmd]) 16 | let output = ""; 17 | subprocess.stdout.setEncoding('utf-8'); 18 | subprocess.stdout.on('data', (data) => { 19 | output += data; 20 | }); 21 | subprocess.stderr.setEncoding('utf-8'); 22 | subprocess.stderr.on('data', (data) => { 23 | output += data; 24 | }); 25 | while (!output.includes("Server ready:")) { 26 | await sleep(500); 27 | } 28 | return {subprocess, output}; 29 | } 30 | 31 | const endpointRegex = /ANY \| (http[^\n\r ]+)/g 32 | 33 | function getEndpoints(info) { 34 | let memo = []; 35 | const matches = info.matchAll(endpointRegex); 36 | for (const m of matches) { 37 | const url = new URL(m[1]); 38 | memo.push(url); 39 | } 40 | return memo; 41 | } 42 | 43 | common.runtimes.forEach(runtime => { 44 | describe(runtime, function () { 45 | this.slow(5000); 46 | this.timeout(10000); 47 | 48 | before(async function() { 49 | this.timeout(0); 50 | process.env.RUNTIME = runtime; 51 | const { subprocess, output } = await runSpawned('offline'); 52 | this.endpoints = getEndpoints(output); 53 | this.subprocess = subprocess; 54 | }); 55 | 56 | after(async function() { 57 | let done = false; 58 | this.subprocess.on('close', () => { 59 | done = true; 60 | }) 61 | kill(this.subprocess.pid); 62 | while (!done) { 63 | await sleep(500); 64 | } 65 | }); 66 | 67 | common.shouldBehaveLikeIntegration(); 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-http-test 2 | 3 | provider: 4 | name: aws 5 | runtime: ${env:RUNTIME} 6 | 7 | plugins: 8 | - serverless-plugin-custom-binary 9 | - serverless-plugin-include-dependencies 10 | - serverless-offline 11 | 12 | custom: 13 | apiGateway: 14 | binaryMediaTypes: 15 | - image/png 16 | 17 | package: 18 | excludeDevDependencies: false 19 | patterns: 20 | - 'test/integration/**' 21 | - '!test.js' 22 | - '!run.sh' 23 | - 'test/integration/image.png' 24 | 25 | functions: 26 | compare: 27 | handler: test/integration/compare.handler 28 | events: 29 | - http: ANY /compare 30 | echo: 31 | handler: test/integration/echo.handler 32 | events: 33 | - http: ANY /echo 34 | - http: 35 | method: ANY 36 | path: /echo/async 37 | async: true 38 | - http: 39 | method: ANY 40 | path: /echo/async/{any+} 41 | async: true 42 | - http: 43 | method: ANY 44 | path: /echo/sync 45 | integration: 'lambda' 46 | - http: 47 | method: ANY 48 | path: /echo/sync/{any+} 49 | integration: 'lambda' 50 | timer: 51 | handler: test/integration/timer.handler 52 | events: 53 | - http: ANY /timer 54 | express: 55 | handler: test/integration/express.handler 56 | events: 57 | - http: ANY /express 58 | - http: ANY /express/{any+} 59 | koa: 60 | handler: test/integration/koa.handler 61 | events: 62 | - http: ANY /koa 63 | cookies: 64 | handler: test/integration/cookies.handler 65 | events: 66 | - http: ANY /cookies 67 | binary: 68 | handler: test/integration/binary.handler 69 | events: 70 | - http: 71 | path: /binary 72 | method: ANY 73 | pino: 74 | handler: test/integration/pino.handler 75 | events: 76 | - http: ANY /pino 77 | root: 78 | handler: test/integration/root.handler 79 | events: 80 | - http: ANY / 81 | -------------------------------------------------------------------------------- /docs/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Note: AWS 4 | 5 | `serverless-http` relies on AWS Lambda Proxy Integration and you can use wildcards in the path binding to delegate routing logic to your application rather than API Gateway. 6 | 7 | The examples below are using the serverless framework (serverless.yml) but the path rules you see would be the same if you set it up manually in the AWS console or via CloudFormation, etc. 8 | 9 | ## All in one 10 | 11 | With this example, the entire API is served by one Lambda function (with two 'events' triggering it - see comments inline) 12 | 13 | ```yml 14 | service: hello-world 15 | 16 | provider: 17 | name: aws 18 | runtime: nodejs8.10 19 | 20 | functions: 21 | api: 22 | handler: src/server.handler 23 | events: 24 | - http: 25 | path: / # this matches the base path 26 | method: ANY 27 | - http: 28 | path: /{any+} # this matches any path, the token 'any' doesn't mean anything special 29 | method: ANY 30 | ``` 31 | 32 | ## Multiple functions/resources 33 | 34 | You do not have to put everything in one function. In the below example there are two separate functions handling two different resources, `/foo` and `/bar` respectively. In this example, `/bar` does not have a second wildcard handler, so ONLY to exact path `/bar` would match. However, `/foo` does have a wildcard so a sub-resource such as `/foo/baz/123` would go to the `foo_api` handler. 35 | 36 | ```yml 37 | service: hello-world 38 | 39 | provider: 40 | name: aws 41 | runtime: nodejs8.10 42 | 43 | functions: 44 | foo_api: 45 | handler: src/foo.handler 46 | events: 47 | - http: 48 | path: /foo # this matches the base path 49 | method: ANY 50 | - http: 51 | path: /foo/{any+} # this matches any path, the token 'any' doesn't mean anything special 52 | method: ANY 53 | bar_api: 54 | handler: src/bar.handler 55 | events: 56 | - http: 57 | path: /bar # this matches the base path only 58 | method: ANY 59 | ``` 60 | -------------------------------------------------------------------------------- /lib/framework/get-framework.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http') 4 | const Response = require('../response'); 5 | 6 | function common(cb) { 7 | return request => { 8 | const response = new Response(request); 9 | 10 | cb(request, response); 11 | 12 | return response; 13 | }; 14 | } 15 | 16 | module.exports = function getFramework(app) { 17 | if (app instanceof http.Server) { 18 | return request => { 19 | const response = new Response(request); 20 | app.emit('request', request, response) 21 | return response 22 | } 23 | } 24 | 25 | if (typeof app.callback === 'function') { 26 | return common(app.callback()); 27 | } 28 | 29 | if (typeof app.handle === 'function') { 30 | return common((request, response) => { 31 | app.handle(request, response); 32 | }); 33 | } 34 | 35 | if (typeof app.handler === 'function') { 36 | return common((request, response) => { 37 | app.handler(request, response); 38 | }); 39 | } 40 | 41 | if (typeof app._onRequest === 'function') { 42 | return common((request, response) => { 43 | app._onRequest(request, response); 44 | }); 45 | } 46 | 47 | if (typeof app === 'function') { 48 | return common(app); 49 | } 50 | 51 | if (app.router && typeof app.router.route == 'function') { 52 | return common((req, res) => { 53 | const { url, method, headers, body } = req; 54 | app.router.route({ url, method, headers, body }, res); 55 | }); 56 | } 57 | 58 | if (app._core && typeof app._core._dispatch === 'function') { 59 | return common(app._core._dispatch({ 60 | app 61 | })); 62 | } 63 | 64 | if (typeof app.inject === 'function') { 65 | return async request => { 66 | const { method, url, headers, body } = request; 67 | 68 | const res = await app.inject({ method, url, headers, payload: body }) 69 | 70 | return Response.from(res); 71 | }; 72 | } 73 | 74 | if (typeof app.main === 'function') { 75 | return common(app.main); 76 | } 77 | 78 | throw new Error('Unsupported framework'); 79 | }; 80 | -------------------------------------------------------------------------------- /lib/provider/aws/format-response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isBinary = require('./is-binary'); 4 | const Response = require('../../response'); 5 | const sanitizeHeaders = require('./sanitize-headers'); 6 | const { getEventType, LAMBDA_EVENT_TYPES } = require('./get-event-type'); 7 | 8 | const combineHeaders = (headers, multiValueHeaders) => { 9 | return Object.entries(headers).reduce((memo, [key, value]) => { 10 | if (multiValueHeaders[key]) { 11 | memo[key].push(value); 12 | } else { 13 | memo[key] = [value]; 14 | } 15 | return memo; 16 | }, multiValueHeaders); 17 | } 18 | 19 | module.exports = (event, response, options) => { 20 | const eventType = getEventType(event); 21 | const { statusCode } = response; 22 | const { headers, multiValueHeaders } = sanitizeHeaders(Response.headers(response)); 23 | 24 | let cookies = []; 25 | 26 | if (multiValueHeaders['set-cookie']) { 27 | cookies = multiValueHeaders['set-cookie']; 28 | } 29 | 30 | const isBase64Encoded = isBinary(headers, options); 31 | const encoding = isBase64Encoded ? 'base64' : 'utf8'; 32 | let body = Response.body(response).toString(encoding); 33 | 34 | if (headers['transfer-encoding'] === 'chunked' || response.chunkedEncoding) { 35 | const raw = Response.body(response).toString().split('\r\n'); 36 | const parsed = []; 37 | for (let i = 0; i < raw.length; i +=2) { 38 | const size = parseInt(raw[i], 16); 39 | const value = raw[i + 1]; 40 | if (value) { 41 | parsed.push(value.substring(0, size)); 42 | } 43 | } 44 | body = parsed.join('') 45 | } 46 | 47 | if (eventType === LAMBDA_EVENT_TYPES.ALB) { 48 | const albResponse = { statusCode, isBase64Encoded, body }; 49 | if (event.multiValueHeaders) { 50 | albResponse.multiValueHeaders = combineHeaders(headers, multiValueHeaders); 51 | } else { 52 | albResponse.headers = headers; 53 | } 54 | return albResponse; 55 | } 56 | 57 | if (eventType === LAMBDA_EVENT_TYPES.HTTP_API_V2) { 58 | return { statusCode, isBase64Encoded, body, headers, cookies }; 59 | } 60 | 61 | // HTTP_API_V1 is the default 62 | return { statusCode, isBase64Encoded, body, headers, multiValueHeaders }; 63 | }; 64 | -------------------------------------------------------------------------------- /test/base-path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const expect = require('chai').expect; 5 | const request = require('./util/request'); 6 | 7 | const expectedFooResponse = 'foo!'; 8 | const expectedBarResponse = 'bar!'; 9 | 10 | describe('base path', () => { 11 | let fooApp; 12 | 13 | beforeEach(() => { 14 | fooApp = express(); 15 | fooApp.get('/', (req, res) => { 16 | return res.send(expectedBarResponse); 17 | }); 18 | fooApp.get('/test', (req, res) => { 19 | return res.send(expectedFooResponse); 20 | }); 21 | }); 22 | 23 | it('should allow for a base path to be set', () => { 24 | return request(fooApp, { 25 | httpMethod: 'GET', 26 | path: '/foo/test' 27 | }, { basePath: '/foo' }) 28 | .then(response => { 29 | expect(response.statusCode).to.equal(200); 30 | expect(response.body).to.equal(expectedFooResponse) 31 | }); 32 | }); 33 | 34 | it('should remove stage path part', () => { 35 | return request(fooApp, { 36 | httpMethod: 'GET', 37 | path: '/dev/foo/test' 38 | }, { basePath: '/foo' }) 39 | .then(response => { 40 | expect(response.statusCode).to.equal(200); 41 | expect(response.body).to.equal(expectedFooResponse) 42 | }); 43 | }); 44 | 45 | [ 46 | '/dev/v1/foo/test', 47 | '/dev/foo/test', 48 | '/foo/test', 49 | '/___/v1/foo/test' 50 | ].map(testCase => { 51 | it(`should work locally and with api gateway (${testCase})`, () => { 52 | return request(fooApp, { 53 | httpMethod: 'GET', 54 | path: testCase 55 | }, { basePath: '/foo' }) 56 | .then(response => { 57 | expect(response.statusCode).to.equal(200); 58 | expect(response.body).to.equal(expectedFooResponse) 59 | }); 60 | }); 61 | }); 62 | 63 | [ 64 | '/dev/foo/', 65 | '/foo/', 66 | '/foo', 67 | '/___/v1/foo/', 68 | '/___/v1/foo' 69 | ].map(testCase => { 70 | it(`should work locally and with api gateway root path (${testCase})`, () => { 71 | return request(fooApp, { 72 | httpMethod: 'GET', 73 | path: testCase 74 | }, { basePath: '/foo' }) 75 | .then(response => { 76 | expect(response.statusCode).to.equal(200); 77 | expect(response.body).to.equal(expectedBarResponse) 78 | }); 79 | }); 80 | }) 81 | }); 82 | -------------------------------------------------------------------------------- /lib/provider/aws/clean-up-event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function removeBasePath(path = '/', basePath) { 4 | if (basePath) { 5 | const basePathIndex = path.indexOf(basePath); 6 | 7 | if (basePathIndex > -1) { 8 | return path.substr(basePathIndex + basePath.length) || '/'; 9 | } 10 | } 11 | 12 | return path; 13 | } 14 | 15 | function isString(value) 16 | { 17 | return (typeof value === 'string' || value instanceof String); 18 | } 19 | 20 | // ELBs will pass spaces as + symbols, and decodeURIComponent doesn't decode + symbols. So we need to convert them into something it can convert 21 | function specialDecodeURIComponent(value) 22 | { 23 | if(!isString(value)) 24 | { 25 | return value; 26 | } 27 | 28 | let decoded; 29 | try { 30 | decoded = decodeURIComponent(value.replace(/[+]/g, "%20")); 31 | } catch (err) { 32 | decoded = value.replace(/[+]/g, "%20"); 33 | } 34 | 35 | return decoded; 36 | } 37 | 38 | function recursiveURLDecode(value) { 39 | 40 | if (isString(value)) { 41 | return specialDecodeURIComponent(value); 42 | } else if (Array.isArray(value)) { 43 | 44 | const decodedArray = []; 45 | 46 | for (let index in value) { 47 | decodedArray.push(recursiveURLDecode(value[index])); 48 | } 49 | 50 | return decodedArray; 51 | 52 | } else if (value instanceof Object) { 53 | 54 | const decodedObject = {}; 55 | 56 | for (let key of Object.keys(value)) { 57 | decodedObject[specialDecodeURIComponent(key)] = recursiveURLDecode(value[key]); 58 | } 59 | 60 | return decodedObject; 61 | } 62 | 63 | return value; 64 | } 65 | 66 | module.exports = function cleanupEvent(evt, options) { 67 | const event = evt || {}; 68 | 69 | event.requestContext = event.requestContext || {}; 70 | event.body = event.body || ''; 71 | event.headers = event.headers || {}; 72 | 73 | // Events coming from AWS Elastic Load Balancers do not automatically urldecode query parameters (unlike API Gateway). So we need to check for that and automatically decode them 74 | // to normalize the request between the two. 75 | if ('elb' in event.requestContext) { 76 | 77 | if (event.multiValueQueryStringParameters) { 78 | event.multiValueQueryStringParameters = recursiveURLDecode(event.multiValueQueryStringParameters); 79 | } 80 | 81 | if (event.queryStringParameters) { 82 | event.queryStringParameters = recursiveURLDecode(event.queryStringParameters); 83 | } 84 | 85 | } 86 | 87 | if (event.version === '2.0') { 88 | event.requestContext.authorizer = event.requestContext.authorizer || {}; 89 | event.requestContext.http.method = event.requestContext.http.method || 'GET'; 90 | event.rawPath = removeBasePath(event.requestPath || event.rawPath, options.basePath); 91 | } else { 92 | event.requestContext.identity = event.requestContext.identity || {}; 93 | event.httpMethod = event.httpMethod || 'GET'; 94 | event.path = removeBasePath(event.requestPath || event.path, options.basePath); 95 | } 96 | 97 | return event; 98 | }; 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-http", 3 | "version": "4.0.0", 4 | "description": "Use existing web application frameworks in serverless environments", 5 | "main": "serverless-http.js", 6 | "types": "serverless-http.d.ts", 7 | "engines": { 8 | "node": ">=12.0" 9 | }, 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/dougmoscrop/serverless-http" 16 | }, 17 | "scripts": { 18 | "pretest": "tsc --strict --skipLibCheck --noEmit test/typecheck.ts", 19 | "test": "nyc mocha", 20 | "posttest": "eslint lib test", 21 | "test:integration:offline": "mocha test/integration/test-offline.js --exit", 22 | "test:integration:aws": "mocha test/integration/test-aws.js --exit", 23 | "postpublish": "git push origin master --tags" 24 | }, 25 | "keywords": [ 26 | "serverless", 27 | "serverless applications", 28 | "koa", 29 | "express", 30 | "connect", 31 | "api gateway", 32 | "lambda", 33 | "aws", 34 | "aws lambda", 35 | "amazon", 36 | "amazon web services" 37 | ], 38 | "author": "Doug Moscrop (http://www.github.com/dougmoscrop)", 39 | "contributors": [ 40 | "Doug Moscrop (https://github.com/dougmoscrop)", 41 | "Kevin Groat (https://github.com/kgroat)", 42 | "Kurt Miller (https://github.com/bsdkurt)", 43 | "Roch Devost (https://github.com/rochdev)", 44 | "Bryan Gamble (https://github.com/bdgamble)", 45 | "Derek MacDonald (https://github.com/demacdonald)", 46 | "Filip Skokan (https://github.com/panva)", 47 | "Kevin Tonon (https://github.com/ktonon)", 48 | "Mark Vayngrib (https://github.com/mvayngrib)", 49 | "Tamás Máhr (https://github.com/tamasmahr)", 50 | "Daniel Martin (https://github.com/daniel-ac-martin)" 51 | ], 52 | "homepage": "https://github.com/dougmoscrop/serverless-http", 53 | "license": "MIT", 54 | "devDependencies": { 55 | "@loopback/rest": "^11.1.2", 56 | "@types/koa": "^2.11.0", 57 | "body-parser": "^1.19.0", 58 | "chai": "^4.2.0", 59 | "chai-as-promised": "^7.1.1", 60 | "cookie-parser": "^1.4.4", 61 | "eslint": "^8.12.0", 62 | "eslint-plugin-mocha": "^10.0.3", 63 | "express": "^4.17.1", 64 | "fastify": "^3.27.4", 65 | "get-stream": "^5.1.0", 66 | "hapi": "^18.1.0", 67 | "inversify": "^6.0.1", 68 | "inversify-express-utils": "^6.3.2", 69 | "koa": "^2.11.0", 70 | "koa-bodyparser": "^4.2.1", 71 | "koa-compress": "^5.1.0", 72 | "koa-route": "^3.2.0", 73 | "koa-router": "^10.1.1", 74 | "koa-static": "^5.0.0", 75 | "lambda-log": "^3.1.0", 76 | "mocha": "^9.2.2", 77 | "morgan": "^1.9.1", 78 | "nyc": "^15.0.0", 79 | "on-finished": "^2.3.0", 80 | "on-headers": "^1.0.2", 81 | "pino": "^8.15.0", 82 | "pino-http": "^8.5.0", 83 | "polka": "^0.5.2", 84 | "reflect-metadata": "^0.1.13", 85 | "restana": "^4.0.7", 86 | "sails": "^1.2.3", 87 | "serverless": "^3.10.2", 88 | "serverless-offline": "^12.0.4", 89 | "serverless-plugin-common-excludes": "^4.0.0", 90 | "serverless-plugin-custom-binary": "^2.0.0", 91 | "serverless-plugin-include-dependencies": "^5.0.0", 92 | "sinon": "^13.0.1", 93 | "supertest": "^6.2.2", 94 | "tree-kill": "^1.2.2", 95 | "typescript": "^4.6.3" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/provider/aws/create-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const URL = require('url'); 4 | 5 | const Request = require('../../request'); 6 | 7 | function requestMethod(event) { 8 | if (event.version === '2.0') { 9 | return event.requestContext.http.method; 10 | } 11 | return event.httpMethod; 12 | } 13 | 14 | function requestRemoteAddress(event) { 15 | if (event.version === '2.0') { 16 | return event.requestContext.http.sourceIp; 17 | } 18 | return event.requestContext.identity.sourceIp; 19 | } 20 | 21 | function requestHeaders(event) { 22 | const initialHeader = 23 | event.version === '2.0' && Array.isArray(event.cookies) 24 | ? { cookie: event.cookies.join('; ') } 25 | : {}; 26 | 27 | if (event.multiValueHeaders) { 28 | Object.keys(event.multiValueHeaders).reduce((headers, key) => { 29 | headers[key.toLowerCase()] = event.multiValueHeaders[key].join(', '); 30 | return headers; 31 | }, initialHeader); 32 | } 33 | 34 | return Object.keys(event.headers).reduce((headers, key) => { 35 | headers[key.toLowerCase()] = event.headers[key]; 36 | return headers; 37 | }, initialHeader); 38 | } 39 | 40 | function requestBody(event) { 41 | const type = typeof event.body; 42 | 43 | if (Buffer.isBuffer(event.body)) { 44 | return event.body; 45 | } else if (type === 'string') { 46 | return Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8'); 47 | } else if (type === 'object') { 48 | return Buffer.from(JSON.stringify(event.body)); 49 | } 50 | 51 | throw new Error(`Unexpected event.body type: ${typeof event.body}`); 52 | } 53 | 54 | function requestUrl(event) { 55 | if (event.version === '2.0') { 56 | return URL.format({ 57 | pathname: event.rawPath, 58 | search: event.rawQueryString, 59 | }); 60 | } 61 | // Normalize all query params into a single query string. 62 | const query = event.multiValueQueryStringParameters || {}; 63 | if (event.queryStringParameters) { 64 | Object.keys(event.queryStringParameters).forEach((key) => { 65 | if (Array.isArray(query[key])) { 66 | if (!query[key].includes(event.queryStringParameters[key])) { 67 | query[key].push(event.queryStringParameters[key]); 68 | } 69 | } else { 70 | query[key] = [event.queryStringParameters[key]]; 71 | } 72 | }); 73 | } 74 | return URL.format({ 75 | pathname: event.path, 76 | query: query, 77 | }); 78 | } 79 | 80 | module.exports = (event, context, options) => { 81 | const method = requestMethod(event); 82 | const remoteAddress = requestRemoteAddress(event); 83 | const headers = requestHeaders(event); 84 | const body = requestBody(event); 85 | const url = requestUrl(event); 86 | 87 | if (typeof options.requestId === 'string' && options.requestId.length > 0) { 88 | const header = options.requestId.toLowerCase(); 89 | const requestId = headers[header] || event.requestContext.requestId; 90 | if (requestId) { 91 | headers[header] = requestId; 92 | } 93 | } 94 | 95 | const req = new Request({ 96 | method, 97 | headers, 98 | body, 99 | remoteAddress, 100 | url, 101 | }); 102 | 103 | req.requestContext = event.requestContext; 104 | req.apiGateway = { 105 | event, 106 | context, 107 | }; 108 | 109 | return req; 110 | }; 111 | -------------------------------------------------------------------------------- /docs/ADVANCED.md: -------------------------------------------------------------------------------- 1 | # Advanced Options 2 | 3 | `serverless-http` takes a second argument, `options`, which can configure: 4 | 5 | ## Request ID 6 | 7 | - **requestId**: the header in which to place the AWS Request ID, defaults to `x-request-id` and can be disabled with `false` 8 | 9 | ```javascript 10 | module.exports.handler = serverless(app, { 11 | requestId: 'X-ReqId', 12 | }); 13 | ``` 14 | 15 | ## Base Path 16 | 17 | - **basePath**: The base path/mount point for your serverless app. Useful if you want to have multiple Lambdas to represent 18 | diffent portions of your application. 19 | 20 | **BEFORE:** 21 | 22 | ```javascript 23 | app.get('/new', (req, res) => { 24 | return res.send('woop'); 25 | }); 26 | 27 | module.exports.handler = serverless(app); 28 | ``` 29 | 30 | ```bash 31 | [GET] http://localhost/transactions/new -> 404 :'( 32 | ``` 33 | 34 | **AFTER:** 35 | 36 | ```javascript 37 | app.get('/new', (req, res) => { 38 | return res.send('woop'); 39 | }); 40 | module.exports.handler = serverless(app, { 41 | basePath: '/transactions' 42 | }); 43 | ``` 44 | 45 | ```bash 46 | [GET] http://localhost/transactions/new -> 200 :+1: 47 | ``` 48 | 49 | **STAGE REMOVAL:** 50 | BasePath will also remove pesky stage information from your URL, so the above example will also work with: 51 | 52 | ```bash 53 | [GET] http://api-gateway.amazonaws.com/dev/v1/transactions/new -> 200! 54 | ``` 55 | 56 | ## Transformations 57 | 58 | - **request**: a *transform* for the request, before it is sent to the app 59 | - **response**: a *transform* for the response, before it is returned to Lambda 60 | 61 | A transform is either a function (req|res, event, context) or an Object to be assigned. 62 | 63 | You can transform the request before it goes through your app. 64 | 65 | You can transform the response after it comes back, before it is sent: 66 | 67 | Some examples: 68 | 69 | ```javascript 70 | module.exports.handler = serverless(app, { 71 | request: { 72 | key: 'value' 73 | }, 74 | response(res) { 75 | res.foo = 'bar'; 76 | } 77 | }) 78 | 79 | module.exports.handler = serverless(app, { 80 | request(request, event, context) { 81 | request.context = event.requestContext; 82 | }, 83 | async response(response, event, context) { 84 | // the return value is always ignored 85 | return new Promise(resolve => { 86 | // override a property of the response, this will affect the response 87 | response.statusCode = 420; 88 | 89 | // delay your responses by 300ms! 90 | setTimeout(300, () => { 91 | // this value has no effect on the response 92 | resolve('banana'); 93 | }); 94 | }); 95 | } 96 | }) 97 | ``` 98 | 99 | ## Binary Mode 100 | 101 | Binary mode detection looks at the `BINARY_CONTENT_TYPES` environment variable, or you can specify an array of types, or a custom function: 102 | 103 | ```js 104 | // turn off any attempt to base64 encode responses -- probably Not Going To Work At All 105 | serverless(app, { 106 | binary: false 107 | }); 108 | 109 | // this is the default, look at content-encoding vs. gzip, deflate and content-type against process.env.BINARY_CONTENT_TYPES 110 | serverless(app, { 111 | binary: true 112 | }); 113 | 114 | // set the types yourself - just like BINARY_CONTENT_TYPES but using an array you pass in, rather than an environment varaible 115 | serverless(app, { 116 | binary: ['application/json', 'image/*'] 117 | }); 118 | 119 | // your own custom callback 120 | serverless(app, { 121 | binary(headers) { 122 | return ... 123 | } 124 | }); 125 | ``` 126 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | 5 | const headerEnd = '\r\n\r\n'; 6 | 7 | const BODY = Symbol(); 8 | const HEADERS = Symbol(); 9 | 10 | function getString(data) { 11 | if (Buffer.isBuffer(data)) { 12 | return data.toString('utf8'); 13 | } else if (typeof data === 'string') { 14 | return data; 15 | } else { 16 | throw new Error(`response.write() of unexpected type: ${typeof data}`); 17 | } 18 | } 19 | 20 | function addData(stream, data) { 21 | if (Buffer.isBuffer(data) || typeof data === 'string' || data instanceof Uint8Array) { 22 | stream[BODY].push(Buffer.from(data)); 23 | } else { 24 | throw new Error(`response.write() of unexpected type: ${typeof data}`); 25 | } 26 | } 27 | 28 | module.exports = class ServerlessResponse extends http.ServerResponse { 29 | 30 | static from(res) { 31 | const response = new ServerlessResponse(res); 32 | 33 | response.statusCode = res.statusCode 34 | response[HEADERS] = res.headers; 35 | response[BODY] = [Buffer.from(res.body)]; 36 | response.end(); 37 | 38 | return response; 39 | } 40 | 41 | static body(res) { 42 | return Buffer.concat(res[BODY]); 43 | } 44 | 45 | static headers(res) { 46 | const headers = typeof res.getHeaders === 'function' 47 | ? res.getHeaders() 48 | : res._headers; 49 | 50 | return Object.assign(headers, res[HEADERS]); 51 | } 52 | 53 | get headers() { 54 | return this[HEADERS]; 55 | } 56 | 57 | setHeader(key, value) { 58 | if (this._wroteHeader) { 59 | this[HEADERS][key] = value; 60 | } else { 61 | super.setHeader(key, value); 62 | } 63 | } 64 | 65 | writeHead(statusCode, reason, obj) { 66 | const headers = typeof reason === 'string' 67 | ? obj 68 | : reason 69 | 70 | for (const name in headers) { 71 | this.setHeader(name, headers[name]) 72 | 73 | if (!this._wroteHeader) { 74 | // we only need to initiate super.headers once 75 | // writeHead will add the other headers itself 76 | break 77 | } 78 | } 79 | 80 | super.writeHead(statusCode, reason, obj); 81 | } 82 | 83 | constructor({ method }) { 84 | super({ method }); 85 | 86 | this[BODY] = []; 87 | this[HEADERS] = {}; 88 | 89 | this.useChunkedEncodingByDefault = false; 90 | this.chunkedEncoding = false; 91 | this._header = ''; 92 | 93 | this.assignSocket({ 94 | _writableState: {}, 95 | writable: true, 96 | on: Function.prototype, 97 | removeListener: Function.prototype, 98 | destroy: Function.prototype, 99 | cork: Function.prototype, 100 | uncork: Function.prototype, 101 | write: (data, encoding, cb) => { 102 | if (typeof encoding === 'function') { 103 | cb = encoding; 104 | encoding = null; 105 | } 106 | 107 | if (this._header === '' || this._wroteHeader) { 108 | addData(this, data); 109 | } else { 110 | const string = getString(data); 111 | const index = string.indexOf(headerEnd); 112 | 113 | if (index !== -1) { 114 | const remainder = string.slice(index + headerEnd.length); 115 | 116 | if (remainder) { 117 | addData(this, remainder); 118 | } 119 | 120 | this._wroteHeader = true; 121 | } 122 | } 123 | 124 | if (typeof cb === 'function') { 125 | cb(); 126 | } 127 | return true; 128 | }, 129 | }); 130 | } 131 | 132 | }; 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-http 2 | 3 | ## Description 4 | 5 | This module allows you to 'wrap' your API for serverless use. No HTTP server, no ports or sockets. Just your code in the same execution pipeline you are already familiar with. 6 | 7 | Note: v4.0.0 doesn't intentionally intorudce any breaking changes, but uses a PassThrough stream internally instead of a stub - however, this is a sensitive area, so in order to avoid unintentional breaks, a semver major version was used. You should hopefully not have to change anything, but test thoroughly please. 8 | 9 | ## Sponsors 10 | 11 | Thank you to Upstash for reaching out to sponsor this project! 12 | 13 | 14 | 15 | 30 | 31 |
16 | 17 | 18 | Upstash 19 | 20 |

Upstash: Serverless Database for Redis

21 | 22 |
    23 |
  • Serverless Redis with global replication and durable storage
  • 24 |
  • Price scales to zero with per request pricing
  • 25 |
  • Built-in REST API designed for serverless and edge functions
  • 26 |
27 | 28 | [Start for free in 30 seconds!](https://upstash.com/?utm_source=serverless-http) 29 |
32 | 33 | ## Support 34 | 35 | ### Supported Frameworks 36 | (* Experimental) 37 | 38 | * Node (http.createServer) 39 | * Connect 40 | * Express 41 | * Koa 42 | * Restana 43 | * Sails * 44 | * Hapi * 45 | * Fastify * 46 | * Restify * 47 | * Polka * 48 | * Loopback * 49 | 50 | ### Supported Providers 51 | 52 | * AWS 53 | * [Genezio](https://genezio.com/deploy-nodejs-express-on-genezio-serverless/) 54 | * Azure (Experimental, untested, probably outdated) 55 | 56 | ## Deploy a Hello Word on Genezio 57 | :rocket: You can deploy your own hello world example using the Express framework to Genezio with one click: 58 | 59 | [![Deploy to Genezio](https://raw.githubusercontent.com/Genez-io/graphics/main/svg/deploy-button.svg)](https://app.genez.io/start/deploy?repository=https://github.com/Genez-io/express-getting-started) 60 | 61 | 62 | 63 | 64 | ## Examples 65 | 66 | Please check the `examples` folder! 67 | 68 | ### Usage example using the Koa framework 69 | 70 | ```javascript 71 | const serverless = require('serverless-http'); 72 | const Koa = require('koa'); // or any supported framework 73 | 74 | const app = new Koa(); 75 | 76 | app.use(/* register your middleware as normal */); 77 | 78 | // this is it! 79 | module.exports.handler = serverless(app); 80 | 81 | // or as a promise 82 | const handler = serverless(app); 83 | module.exports.handler = async (event, context) => { 84 | // you can do other things here 85 | const result = await handler(event, context); 86 | // and here 87 | return result; 88 | }; 89 | ``` 90 | 91 | ### Usage example using the Express framework with Azure 92 | 93 | ```javascript 94 | 95 | const serverless = require('serverless-http'); 96 | const express = require('express'); 97 | 98 | const app = express(); 99 | 100 | app.use(/* register your middleware as normal */); 101 | 102 | const handler = serverless(app, { provider: 'azure' }); 103 | module.exports.funcName = async (context, req) => { 104 | context.res = await handler(context, req); 105 | } 106 | 107 | ``` 108 | 109 | ### Other examples 110 | [json-server-less-λ](https://github.com/pharindoko/json-server-less-lambda) - using serverless-http with json-server and serverless framework in AWS 111 | 112 | 113 | ## Limitations 114 | 115 | Your code is running in a serverless environment. You cannot rely on your server being 'up' in the sense that you can/should not use in-memory sessions, web sockets, etc. You are also subject to provider specific restrictions on request/response size, duration, etc. 116 | 117 | > Think of this as a familiar way of expressing your app logic, *not* trying to make serverless do something it cannot. 118 | 119 | ## Contributing 120 | 121 | Pull requests are welcome! Especially test scenarios for different situations and configurations. 122 | 123 | ## Further Reading 124 | 125 | Here are some [more detailed examples](./docs/EXAMPLES.md) and [advanced configuration options](./docs/ADVANCED.md) as well as [provider-specific documentation](./docs/PROVIDERS.md) 126 | 127 | -------------------------------------------------------------------------------- /test/restana.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const restana = require('restana'), 4 | bodyParser = require('body-parser'), 5 | morgan = require('morgan'), 6 | expect = require('chai').expect, 7 | url = require('url'), 8 | request = require('./util/request'); 9 | 10 | describe('restana', () => { 11 | let app; 12 | 13 | beforeEach(function() { 14 | app = restana(); 15 | }); 16 | 17 | it('basic middleware should set statusCode and default body', () => { 18 | app.use(function (req, res) { 19 | res.send(`I'm a teapot`, 418); 20 | }); 21 | 22 | return request(app, { 23 | httpMethod: 'GET' 24 | }) 25 | .then(response => { 26 | expect(response.statusCode).to.equal(418); 27 | expect(response.body).to.equal(`I'm a teapot`) 28 | }); 29 | }); 30 | 31 | it('basic middleware should get text body', () => { 32 | app.use(bodyParser.text()); 33 | app.use(function (req, res) { 34 | res.send(req.body); 35 | }); 36 | 37 | return request(app, { 38 | httpMethod: 'GET', 39 | body: 'hello, world', 40 | headers: { 41 | 'Content-Type': 'text/plain', 42 | 'Content-Length': '12' 43 | } 44 | }) 45 | .then(response => { 46 | expect(response.statusCode).to.equal(200); 47 | expect(response.body).to.equal('hello, world') 48 | }); 49 | }); 50 | 51 | it('basic middleware should get json body', () => { 52 | app.use(bodyParser.json()); 53 | app.use(function (req, res) { 54 | res.send(req.body.hello); 55 | }); 56 | 57 | return request(app, { 58 | httpMethod: 'GET', 59 | body: JSON.stringify({ 60 | hello: 'world' 61 | }), 62 | headers: { 63 | 'Content-Type': 'application/json' 64 | } 65 | }) 66 | .then(response => { 67 | expect(response.statusCode).to.equal(200); 68 | expect(response.body).to.equal('world'); 69 | }); 70 | }); 71 | 72 | it('basic middleware should get query params', () => { 73 | app.use(function (req, res) { 74 | const {query} = url.parse(req.url, true) 75 | res.send(query.foo); 76 | }); 77 | 78 | return request(app, { 79 | httpMethod: 'GET', 80 | path: '/', 81 | queryStringParameters: { 82 | foo: 'bar' 83 | } 84 | }) 85 | .then(response => { 86 | expect(response.statusCode).to.equal(200); 87 | expect(response.body).to.equal('bar') 88 | }); 89 | }); 90 | 91 | it('should match verbs', () => { 92 | app.get('/', function(req, res) { 93 | res.send('foo'); 94 | }); 95 | app.put('/', function(req, res) { 96 | res.send('bar', 201); 97 | }); 98 | 99 | return request(app, { 100 | httpMethod: 'PUT' 101 | }) 102 | .then(response => { 103 | expect(response.statusCode).to.equal(201); 104 | expect(response.body).to.equal('bar'); 105 | }); 106 | }); 107 | 108 | describe('morgan', () => { 109 | it('combined', () => { 110 | app.use(morgan('combined')); 111 | app.use((req, res) => { 112 | res.send('hello, morgan'); 113 | }); 114 | 115 | return request(app, { 116 | httpMethod: 'GET', 117 | headers: { 118 | authorization: 'Basic QWxhZGRpbjpPcGVuU2VzYW1l' 119 | }, 120 | path: '/', 121 | requestContext: { 122 | identity: { 123 | sourceIp: '1.3.3.7' 124 | } 125 | } 126 | }) 127 | .then(response => { 128 | expect(response.statusCode).to.equal(200); 129 | }); 130 | }); 131 | 132 | it('short', () => { 133 | app.use(morgan('short')); 134 | app.use((req, res) => { 135 | res.send('hello, morgan'); 136 | }); 137 | 138 | return request(app, { 139 | httpMethod: 'GET', 140 | headers: { 141 | authorization: 'Basic QWxhZGRpbjpPcGVuU2VzYW1l' 142 | }, 143 | requestContext: { 144 | identity: { 145 | sourceIp: '1.3.3.7' 146 | } 147 | } 148 | }) 149 | .then(response => { 150 | expect(response.statusCode).to.equal(200); 151 | }); 152 | }); 153 | }); 154 | 155 | it('address() returns a stubbed object', () => { 156 | app.use(morgan('short')); 157 | app.use((req, res) => { 158 | res.send(req.connection.address()); 159 | }); 160 | 161 | return request(app, { 162 | httpMethod: 'GET' 163 | }) 164 | .then(response => { 165 | expect(response.statusCode).to.equal(200); 166 | expect(response.body).to.equal(JSON.stringify({ port: 443 })); 167 | }); 168 | }); 169 | 170 | }); 171 | -------------------------------------------------------------------------------- /test/generic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const url = require('url'), 4 | fs = require('fs'), 5 | expect = require('chai').expect, 6 | request = require('./util/request'), 7 | sinon = require('sinon'); 8 | 9 | describe('generic http listener', () => { 10 | let app; 11 | 12 | it('should set statusCode and default body', () => { 13 | app = function(req, res) { 14 | res.statusCode = 418; 15 | res.write('I\'m a teapot'); 16 | res.end(); 17 | }; 18 | 19 | return request(app, { 20 | httpMethod: 'GET', 21 | path: '/' 22 | }) 23 | .then(response => { 24 | expect(response.statusCode).to.equal(418); 25 | expect(response.body).to.equal('I\'m a teapot'); 26 | }); 27 | }); 28 | 29 | it('should get / set body', () => { 30 | app = function(req, res) { 31 | let body = ''; 32 | 33 | req.on('data', function(data) { 34 | body += data; 35 | }); 36 | 37 | req.on('end', function() { 38 | res.statusCode = 200; 39 | res.write(body); 40 | res.end(); 41 | }); 42 | }; 43 | 44 | return request(app, { 45 | httpMethod: 'GET', 46 | body: 'hello, world' 47 | }) 48 | .then(response => { 49 | expect(response.statusCode).to.equal(200); 50 | expect(response.body).to.equal('hello, world'); 51 | }); 52 | }); 53 | 54 | it('should get query params', () => { 55 | app = function (req, res) { 56 | const urlObject = url.parse(req.url); 57 | 58 | res.statusCode = 200; 59 | res.write(urlObject.query); 60 | res.end(); 61 | }; 62 | 63 | return request(app, { 64 | httpMethod: 'GET', 65 | queryStringParameters: { 66 | foo: 'bar' 67 | } 68 | }) 69 | .then(response => { 70 | expect(response.statusCode).to.equal(200); 71 | expect(response.body).to.equal('foo=bar'); 72 | }); 73 | }); 74 | 75 | it('should get multi-value query params', () => { 76 | app = function (req, res) { 77 | const urlObject = url.parse(req.url); 78 | 79 | res.statusCode = 200; 80 | res.write(urlObject.query); 81 | res.end(); 82 | }; 83 | 84 | return request(app, { 85 | httpMethod: 'GET', 86 | queryStringParameters: { 87 | foo: 'bar' 88 | }, 89 | multiValueQueryStringParameters: { 90 | foo: ['qux', 'bar'] 91 | } 92 | }) 93 | .then(response => { 94 | expect(response.statusCode).to.equal(200); 95 | expect(response.body).to.equal('foo=qux&foo=bar'); 96 | }); 97 | }); 98 | 99 | it('should match verbs', () => { 100 | app = function (req, res) { 101 | if (req.method === 'PUT') { 102 | res.statusCode = 200; 103 | res.write('foo'); 104 | res.end(); 105 | } 106 | }; 107 | 108 | return request(app, { 109 | httpMethod: 'PUT', 110 | path: '/' 111 | }) 112 | .then(response => { 113 | expect(response.statusCode).to.equal(200); 114 | expect(response.body).to.equal('foo'); 115 | }); 116 | }); 117 | 118 | it('should serve files', () => { 119 | app = function (req, res) { 120 | const fileStream = fs.createReadStream('test/file.txt'); 121 | 122 | res.statusCode = 200; 123 | fileStream.pipe(res); 124 | }; 125 | 126 | return request(app, { 127 | httpMethod: 'GET', 128 | path: '/file.txt' 129 | }) 130 | .then(response => { 131 | expect(response.statusCode).to.equal(200); 132 | expect(response.body).to.equal('this is a test\n'); 133 | }); 134 | }); 135 | 136 | it('should intercept writeHead', () => { 137 | app = function(req, res) { 138 | res.writeHead(302, { 139 | Location: '/redirect', 140 | Accept: '*/*' 141 | }); 142 | res.end(); 143 | }; 144 | 145 | return request(app, { 146 | httpMethod: 'GET', 147 | path: '/' 148 | }) 149 | .then(response => { 150 | expect(response.statusCode).to.equal(302); 151 | expect(response.headers).to.deep.equal({ 152 | location: '/redirect', 153 | accept: '*/*' 154 | }); 155 | }); 156 | }); 157 | 158 | it('should emit close once', () => { 159 | const stub = sinon.stub().returns(true); 160 | app = function (req, res) { 161 | res.on('close', stub); 162 | res.writeHead(200); 163 | res.emit('finish'); // Test multiple finish deliveries 164 | res.end(); 165 | }; 166 | 167 | return request(app, { 168 | httpMethod: 'GET', 169 | path: '/', 170 | }).then(() => { 171 | expect(stub.calledOnce).to.be.true; 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/integration/test-common.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const supertest = require('supertest'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | exports.runtimes = [ 7 | 'nodejs12.x', 8 | 'nodejs14.x', 9 | ]; 10 | 11 | 12 | exports.shouldBehaveLikeIntegration = function() { 13 | before(function() { 14 | this.getEndpoint = (path) => { 15 | return this.endpoints.find(e => e.pathname === path); 16 | }; 17 | this.logs = ""; 18 | if (this.subprocess !== undefined) { 19 | this.subprocess.stdout.on("data", (data) => { 20 | this.logs += data; 21 | }); 22 | this.subprocess.stderr.on("data", (data) => { 23 | this.logs += data; 24 | }); 25 | } 26 | }); 27 | 28 | beforeEach(function() { 29 | this.logs = ""; // clear any previous test logs 30 | }) 31 | 32 | describe('koa', function() { 33 | it('get', function() { 34 | const endpoint = this.getEndpoint('/dev/koa'); 35 | 36 | return supertest(endpoint.origin) 37 | .get(endpoint.pathname) 38 | .expect(200) 39 | .expect('Content-Type', /json/) 40 | .then(response => { 41 | expect(response.body.url).to.equal('/koa'); 42 | expect(response.body.method).to.equal('get'); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('express', function() { 48 | 49 | ['get', 'put', 'post'].forEach(method => { 50 | it(method, function() { 51 | const endpoint = this.getEndpoint('/dev/express'); 52 | 53 | return supertest(endpoint.origin)[method](endpoint.pathname) 54 | .expect(200) 55 | .expect('Content-Type', /json/) 56 | .then(response => { 57 | expect(response.body.originalUrl).to.equal('/express'); 58 | expect(response.body.url).to.equal('/express'); 59 | expect(response.body.method).to.equal(method); 60 | }); 61 | }); 62 | }); 63 | 64 | it('get-with-path', function() { 65 | const endpoint = this.getEndpoint('/dev/express'); 66 | 67 | return supertest(endpoint.origin) 68 | .get(`${endpoint.pathname}/pathed/1`) 69 | .expect(200) 70 | .expect('Content-Type', /json/) 71 | .then(response => { 72 | expect(response.body.originalUrl).to.equal('/express/pathed/1'); 73 | expect(response.body.url).to.equal('/express/pathed/1'); 74 | expect(response.body.method).to.equal('get'); 75 | expect(response.body.id).to.equal('1'); 76 | }); 77 | }); 78 | 79 | }); 80 | 81 | it('binary', function() { 82 | const endpoint = this.getEndpoint('/dev/binary'); 83 | 84 | const imagePath = path.join(__dirname, 'image.png'); 85 | const expected = fs.readFileSync(imagePath); 86 | 87 | return supertest(endpoint.origin) 88 | .get(endpoint.pathname) 89 | .set('Accept', 'image/png') // if this is image/*, APIg will not match :( 90 | .expect(200) 91 | .expect('Content-Type', /png/) 92 | .then(response => { 93 | if (Buffer.isBuffer(response.body)) { 94 | if (response.body.equals(expected)) { 95 | return; 96 | } 97 | } 98 | 99 | throw new Error('Binary response body was not a buffer or not equal to the expected image'); 100 | }); 101 | }); 102 | 103 | it('timer', function() { 104 | const endpoint = this.getEndpoint('/dev/timer'); 105 | 106 | return supertest(endpoint.origin) 107 | .get(endpoint.pathname) 108 | .expect(200) 109 | .expect('Content-Type', /json/); 110 | }); 111 | 112 | it('pino', function(done) { 113 | const endpoint = this.getEndpoint('/dev/pino'); 114 | 115 | supertest(endpoint.origin) 116 | .get(endpoint.pathname) 117 | .expect(200) 118 | .expect('Content-Type', /json/) 119 | .end(() => { 120 | // In the AWS case, we have no logs 121 | if (this.logs === "") { 122 | return done(); 123 | } 124 | // Should only log once 125 | const matchCount = (this.logs.match(/"statusCode":200/g) || []).length; 126 | if (matchCount !==1) { 127 | console.log("logs", this.logs); 128 | } 129 | expect(matchCount).to.equal(1); 130 | return done(); 131 | }); 132 | }); 133 | 134 | // FIXME: Broken currently https://github.com/dougmoscrop/serverless-http/issues/270 135 | it.skip('root', function() { 136 | const endpoint = this.getEndpoint('/dev'); 137 | 138 | return supertest(endpoint.origin) 139 | .get(endpoint.pathname) 140 | .expect(200) 141 | .expect('Content-Type', /json/); 142 | }); 143 | }; -------------------------------------------------------------------------------- /test/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'), 4 | bodyParser = require('body-parser'), 5 | morgan = require('morgan'), 6 | expect = require('chai').expect, 7 | request = require('./util/request'); 8 | 9 | describe('express', () => { 10 | let app; 11 | 12 | beforeEach(function() { 13 | app = express(); 14 | }); 15 | 16 | it('basic middleware should set statusCode and default body', () => { 17 | app.use(function (req, res) { 18 | res.status(418).send(`I'm a teapot`); 19 | }); 20 | 21 | return request(app, { 22 | httpMethod: 'GET' 23 | }) 24 | .then(response => { 25 | expect(response.statusCode).to.equal(418); 26 | expect(response.body).to.equal(`I'm a teapot`) 27 | }); 28 | }); 29 | 30 | it('basic middleware should get text body', () => { 31 | app.use(bodyParser.text()); 32 | app.use(function (req, res) { 33 | res.status(200).send(req.body); 34 | }); 35 | 36 | return request(app, { 37 | httpMethod: 'GET', 38 | body: 'hello, world', 39 | headers: { 40 | 'Content-Type': 'text/plain', 41 | 'Content-Length': '12' 42 | } 43 | }) 44 | .then(response => { 45 | expect(response.statusCode).to.equal(200); 46 | expect(response.body).to.equal('hello, world') 47 | }); 48 | }); 49 | 50 | it('basic middleware should get json body', () => { 51 | app.use(bodyParser.json()); 52 | app.use(function (req, res) { 53 | res.status(200).send(req.body.hello); 54 | }); 55 | 56 | return request(app, { 57 | httpMethod: 'GET', 58 | body: JSON.stringify({ 59 | hello: 'world' 60 | }), 61 | headers: { 62 | 'Content-Type': 'application/json' 63 | } 64 | }) 65 | .then(response => { 66 | expect(response.statusCode).to.equal(200); 67 | expect(response.body).to.equal('world'); 68 | }); 69 | }); 70 | 71 | it('basic middleware should get query params', () => { 72 | app.use(function (req, res) { 73 | res.status(200).send(req.query.foo); 74 | }); 75 | 76 | return request(app, { 77 | httpMethod: 'GET', 78 | path: '/', 79 | queryStringParameters: { 80 | foo: 'bar' 81 | } 82 | }) 83 | .then(response => { 84 | expect(response.statusCode).to.equal(200); 85 | expect(response.body).to.equal('bar') 86 | }); 87 | }); 88 | 89 | it('should match verbs', () => { 90 | app.get('/', function(req, res) { 91 | res.status(200).send('foo'); 92 | }); 93 | app.put('/', function(req, res) { 94 | res.status(201).send('bar'); 95 | }); 96 | 97 | return request(app, { 98 | httpMethod: 'PUT' 99 | }) 100 | .then(response => { 101 | expect(response.statusCode).to.equal(201); 102 | expect(response.body).to.equal('bar'); 103 | }); 104 | }); 105 | 106 | it('should serve files', () => { 107 | app.use(express.static('test')); 108 | 109 | return request(app, { 110 | httpMethod: 'GET', 111 | path: '/file.txt' 112 | }) 113 | .then(response => { 114 | expect(response.statusCode).to.equal(200); 115 | expect(response.body).to.equal('this is a test\n'); 116 | }); 117 | }); 118 | 119 | describe('morgan', () => { 120 | it('combined', () => { 121 | app.use(morgan('combined')); 122 | app.use((req, res) => { 123 | res.status(200).send('hello, morgan'); 124 | }); 125 | 126 | return request(app, { 127 | httpMethod: 'GET', 128 | headers: { 129 | authorization: 'Basic QWxhZGRpbjpPcGVuU2VzYW1l' 130 | }, 131 | path: '/', 132 | requestContext: { 133 | identity: { 134 | sourceIp: '1.3.3.7' 135 | } 136 | } 137 | }) 138 | .then(response => { 139 | expect(response.statusCode).to.equal(200); 140 | }); 141 | }); 142 | 143 | it('short', () => { 144 | app.use(morgan('short')); 145 | app.use((req, res) => { 146 | res.status(200).send('hello, morgan'); 147 | }); 148 | 149 | return request(app, { 150 | httpMethod: 'GET', 151 | headers: { 152 | authorization: 'Basic QWxhZGRpbjpPcGVuU2VzYW1l' 153 | }, 154 | requestContext: { 155 | identity: { 156 | sourceIp: '1.3.3.7' 157 | } 158 | } 159 | }) 160 | .then(response => { 161 | expect(response.statusCode).to.equal(200); 162 | }); 163 | }); 164 | }); 165 | 166 | it('address() returns a stubbed object', () => { 167 | app.use(morgan('short')); 168 | app.use((req, res) => { 169 | res.status(200).send(req.connection.address()); 170 | }); 171 | 172 | return request(app, { 173 | httpMethod: 'GET' 174 | }) 175 | .then(response => { 176 | expect(response.statusCode).to.equal(200); 177 | expect(response.body).to.equal(JSON.stringify({ port: 443 })); 178 | }); 179 | }); 180 | 181 | it('destroy weird', () => { 182 | app.use((req, res) => { 183 | // this was causing a .destroy is not a function error 184 | res.send('test'); 185 | res.json({ test: 'test' }); 186 | }); 187 | 188 | return request(app, { 189 | httpMethod: 'GET', 190 | path: '/bar', 191 | requestContext: { 192 | path: '/foo/bar' 193 | } 194 | }) 195 | .then(response => { 196 | expect(response.statusCode).to.equal(200); 197 | expect(response.body).to.equal('test'); 198 | }); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /lib/provider/azure/set-cookie.json: -------------------------------------------------------------------------------- 1 | {"variations":["set-cookie","Set-cookie","sEt-cookie","SEt-cookie","seT-cookie","SeT-cookie","sET-cookie","SET-cookie","set-Cookie","Set-Cookie","sEt-Cookie","SEt-Cookie","seT-Cookie","SeT-Cookie","sET-Cookie","SET-Cookie","set-cOokie","Set-cOokie","sEt-cOokie","SEt-cOokie","seT-cOokie","SeT-cOokie","sET-cOokie","SET-cOokie","set-COokie","Set-COokie","sEt-COokie","SEt-COokie","seT-COokie","SeT-COokie","sET-COokie","SET-COokie","set-coOkie","Set-coOkie","sEt-coOkie","SEt-coOkie","seT-coOkie","SeT-coOkie","sET-coOkie","SET-coOkie","set-CoOkie","Set-CoOkie","sEt-CoOkie","SEt-CoOkie","seT-CoOkie","SeT-CoOkie","sET-CoOkie","SET-CoOkie","set-cOOkie","Set-cOOkie","sEt-cOOkie","SEt-cOOkie","seT-cOOkie","SeT-cOOkie","sET-cOOkie","SET-cOOkie","set-COOkie","Set-COOkie","sEt-COOkie","SEt-COOkie","seT-COOkie","SeT-COOkie","sET-COOkie","SET-COOkie","set-cooKie","Set-cooKie","sEt-cooKie","SEt-cooKie","seT-cooKie","SeT-cooKie","sET-cooKie","SET-cooKie","set-CooKie","Set-CooKie","sEt-CooKie","SEt-CooKie","seT-CooKie","SeT-CooKie","sET-CooKie","SET-CooKie","set-cOoKie","Set-cOoKie","sEt-cOoKie","SEt-cOoKie","seT-cOoKie","SeT-cOoKie","sET-cOoKie","SET-cOoKie","set-COoKie","Set-COoKie","sEt-COoKie","SEt-COoKie","seT-COoKie","SeT-COoKie","sET-COoKie","SET-COoKie","set-coOKie","Set-coOKie","sEt-coOKie","SEt-coOKie","seT-coOKie","SeT-coOKie","sET-coOKie","SET-coOKie","set-CoOKie","Set-CoOKie","sEt-CoOKie","SEt-CoOKie","seT-CoOKie","SeT-CoOKie","sET-CoOKie","SET-CoOKie","set-cOOKie","Set-cOOKie","sEt-cOOKie","SEt-cOOKie","seT-cOOKie","SeT-cOOKie","sET-cOOKie","SET-cOOKie","set-COOKie","Set-COOKie","sEt-COOKie","SEt-COOKie","seT-COOKie","SeT-COOKie","sET-COOKie","SET-COOKie","set-cookIe","Set-cookIe","sEt-cookIe","SEt-cookIe","seT-cookIe","SeT-cookIe","sET-cookIe","SET-cookIe","set-CookIe","Set-CookIe","sEt-CookIe","SEt-CookIe","seT-CookIe","SeT-CookIe","sET-CookIe","SET-CookIe","set-cOokIe","Set-cOokIe","sEt-cOokIe","SEt-cOokIe","seT-cOokIe","SeT-cOokIe","sET-cOokIe","SET-cOokIe","set-COokIe","Set-COokIe","sEt-COokIe","SEt-COokIe","seT-COokIe","SeT-COokIe","sET-COokIe","SET-COokIe","set-coOkIe","Set-coOkIe","sEt-coOkIe","SEt-coOkIe","seT-coOkIe","SeT-coOkIe","sET-coOkIe","SET-coOkIe","set-CoOkIe","Set-CoOkIe","sEt-CoOkIe","SEt-CoOkIe","seT-CoOkIe","SeT-CoOkIe","sET-CoOkIe","SET-CoOkIe","set-cOOkIe","Set-cOOkIe","sEt-cOOkIe","SEt-cOOkIe","seT-cOOkIe","SeT-cOOkIe","sET-cOOkIe","SET-cOOkIe","set-COOkIe","Set-COOkIe","sEt-COOkIe","SEt-COOkIe","seT-COOkIe","SeT-COOkIe","sET-COOkIe","SET-COOkIe","set-cooKIe","Set-cooKIe","sEt-cooKIe","SEt-cooKIe","seT-cooKIe","SeT-cooKIe","sET-cooKIe","SET-cooKIe","set-CooKIe","Set-CooKIe","sEt-CooKIe","SEt-CooKIe","seT-CooKIe","SeT-CooKIe","sET-CooKIe","SET-CooKIe","set-cOoKIe","Set-cOoKIe","sEt-cOoKIe","SEt-cOoKIe","seT-cOoKIe","SeT-cOoKIe","sET-cOoKIe","SET-cOoKIe","set-COoKIe","Set-COoKIe","sEt-COoKIe","SEt-COoKIe","seT-COoKIe","SeT-COoKIe","sET-COoKIe","SET-COoKIe","set-coOKIe","Set-coOKIe","sEt-coOKIe","SEt-coOKIe","seT-coOKIe","SeT-coOKIe","sET-coOKIe","SET-coOKIe","set-CoOKIe","Set-CoOKIe","sEt-CoOKIe","SEt-CoOKIe","seT-CoOKIe","SeT-CoOKIe","sET-CoOKIe","SET-CoOKIe","set-cOOKIe","Set-cOOKIe","sEt-cOOKIe","SEt-cOOKIe","seT-cOOKIe","SeT-cOOKIe","sET-cOOKIe","SET-cOOKIe","set-COOKIe","Set-COOKIe","sEt-COOKIe","SEt-COOKIe","seT-COOKIe","SeT-COOKIe","sET-COOKIe","SET-COOKIe","set-cookiE","Set-cookiE","sEt-cookiE","SEt-cookiE","seT-cookiE","SeT-cookiE","sET-cookiE","SET-cookiE","set-CookiE","Set-CookiE","sEt-CookiE","SEt-CookiE","seT-CookiE","SeT-CookiE","sET-CookiE","SET-CookiE","set-cOokiE","Set-cOokiE","sEt-cOokiE","SEt-cOokiE","seT-cOokiE","SeT-cOokiE","sET-cOokiE","SET-cOokiE","set-COokiE","Set-COokiE","sEt-COokiE","SEt-COokiE","seT-COokiE","SeT-COokiE","sET-COokiE","SET-COokiE","set-coOkiE","Set-coOkiE","sEt-coOkiE","SEt-coOkiE","seT-coOkiE","SeT-coOkiE","sET-coOkiE","SET-coOkiE","set-CoOkiE","Set-CoOkiE","sEt-CoOkiE","SEt-CoOkiE","seT-CoOkiE","SeT-CoOkiE","sET-CoOkiE","SET-CoOkiE","set-cOOkiE","Set-cOOkiE","sEt-cOOkiE","SEt-cOOkiE","seT-cOOkiE","SeT-cOOkiE","sET-cOOkiE","SET-cOOkiE","set-COOkiE","Set-COOkiE","sEt-COOkiE","SEt-COOkiE","seT-COOkiE","SeT-COOkiE","sET-COOkiE","SET-COOkiE","set-cooKiE","Set-cooKiE","sEt-cooKiE","SEt-cooKiE","seT-cooKiE","SeT-cooKiE","sET-cooKiE","SET-cooKiE","set-CooKiE","Set-CooKiE","sEt-CooKiE","SEt-CooKiE","seT-CooKiE","SeT-CooKiE","sET-CooKiE","SET-CooKiE","set-cOoKiE","Set-cOoKiE","sEt-cOoKiE","SEt-cOoKiE","seT-cOoKiE","SeT-cOoKiE","sET-cOoKiE","SET-cOoKiE","set-COoKiE","Set-COoKiE","sEt-COoKiE","SEt-COoKiE","seT-COoKiE","SeT-COoKiE","sET-COoKiE","SET-COoKiE","set-coOKiE","Set-coOKiE","sEt-coOKiE","SEt-coOKiE","seT-coOKiE","SeT-coOKiE","sET-coOKiE","SET-coOKiE","set-CoOKiE","Set-CoOKiE","sEt-CoOKiE","SEt-CoOKiE","seT-CoOKiE","SeT-CoOKiE","sET-CoOKiE","SET-CoOKiE","set-cOOKiE","Set-cOOKiE","sEt-cOOKiE","SEt-cOOKiE","seT-cOOKiE","SeT-cOOKiE","sET-cOOKiE","SET-cOOKiE","set-COOKiE","Set-COOKiE","sEt-COOKiE","SEt-COOKiE","seT-COOKiE","SeT-COOKiE","sET-COOKiE","SET-COOKiE","set-cookIE","Set-cookIE","sEt-cookIE","SEt-cookIE","seT-cookIE","SeT-cookIE","sET-cookIE","SET-cookIE","set-CookIE","Set-CookIE","sEt-CookIE","SEt-CookIE","seT-CookIE","SeT-CookIE","sET-CookIE","SET-CookIE","set-cOokIE","Set-cOokIE","sEt-cOokIE","SEt-cOokIE","seT-cOokIE","SeT-cOokIE","sET-cOokIE","SET-cOokIE","set-COokIE","Set-COokIE","sEt-COokIE","SEt-COokIE","seT-COokIE","SeT-COokIE","sET-COokIE","SET-COokIE","set-coOkIE","Set-coOkIE","sEt-coOkIE","SEt-coOkIE","seT-coOkIE","SeT-coOkIE","sET-coOkIE","SET-coOkIE","set-CoOkIE","Set-CoOkIE","sEt-CoOkIE","SEt-CoOkIE","seT-CoOkIE","SeT-CoOkIE","sET-CoOkIE","SET-CoOkIE","set-cOOkIE","Set-cOOkIE","sEt-cOOkIE","SEt-cOOkIE","seT-cOOkIE","SeT-cOOkIE","sET-cOOkIE","SET-cOOkIE","set-COOkIE","Set-COOkIE","sEt-COOkIE","SEt-COOkIE","seT-COOkIE","SeT-COOkIE","sET-COOkIE","SET-COOkIE","set-cooKIE","Set-cooKIE","sEt-cooKIE","SEt-cooKIE","seT-cooKIE","SeT-cooKIE","sET-cooKIE","SET-cooKIE","set-CooKIE","Set-CooKIE","sEt-CooKIE","SEt-CooKIE","seT-CooKIE","SeT-CooKIE","sET-CooKIE","SET-CooKIE","set-cOoKIE","Set-cOoKIE","sEt-cOoKIE","SEt-cOoKIE","seT-cOoKIE","SeT-cOoKIE","sET-cOoKIE","SET-cOoKIE","set-COoKIE","Set-COoKIE","sEt-COoKIE","SEt-COoKIE","seT-COoKIE","SeT-COoKIE","sET-COoKIE","SET-COoKIE","set-coOKIE","Set-coOKIE","sEt-coOKIE","SEt-coOKIE","seT-coOKIE","SeT-coOKIE","sET-coOKIE","SET-coOKIE","set-CoOKIE","Set-CoOKIE","sEt-CoOKIE","SEt-CoOKIE","seT-CoOKIE","SeT-CoOKIE","sET-CoOKIE","SET-CoOKIE","set-cOOKIE","Set-cOOKIE","sEt-cOOKIE","SEt-cOOKIE","seT-cOOKIE","SeT-cOOKIE","sET-cOOKIE","SET-cOOKIE","set-COOKIE","Set-COOKIE","sEt-COOKIE","SEt-COOKIE","seT-COOKIE","SeT-COOKIE","sET-COOKIE","SET-COOKIE"]} -------------------------------------------------------------------------------- /test/format-response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const formatResponse = require('../lib/provider/aws/format-response'); 4 | const Response = require('../lib/response'); 5 | const expect = require('chai').expect; 6 | 7 | describe('format-response', function () { 8 | 9 | // Construct dummy v1 event 10 | const v1Event = { 11 | version: '1.0', 12 | resource: '/my/path', 13 | path: '/my/path', 14 | httpMethod: 'GET', 15 | headers: { 16 | 'Header1': 'value1', 17 | 'Header2': 'value2' 18 | }, 19 | queryStringParameters: { parameter1: 'value1', parameter2: 'value' }, 20 | multiValueQueryStringParameters: { parameter1: ['value1', 'value2'], paramter2: ['value'] }, 21 | requestContext: { 22 | accountId: '123456789012', 23 | apiId: 'id', 24 | authorizer: { claims: null, scopes: null }, 25 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 26 | domainPrefix: 'id', 27 | extendedRequestId: 'request-id', 28 | httpMethod: 'GET', 29 | path: '/my/path', 30 | protocol: 'HTTP/1.1', 31 | requestId: 'id=', 32 | requestTime: '04/Mar/2020:19:15:17 +0000', 33 | requestTimeEpoch: 1583349317135, 34 | resourceId: null, 35 | resourcePath: '/my/path', 36 | stage: '$default' 37 | }, 38 | pathParameters: null, 39 | stageVariables: null, 40 | body: 'Hello from Lambda!', 41 | isBase64Encoded: true 42 | }; 43 | 44 | // Construct dummy v2 event 45 | const v2Event = { 46 | version: '2.0', 47 | routeKey: '$default', 48 | rawPath: '/my/path', 49 | rawQueryString: 'parameter1=value1¶meter1=value2¶meter2=value', 50 | cookies: ['cookie1', 'cookie2'], 51 | headers: { 52 | 'Header1': 'value1', 53 | 'Header2': 'value2' 54 | }, 55 | queryStringParameters: { parameter1: 'value1,value2', parameter2: 'value' }, 56 | requestContext: { 57 | accountId: '123456789012', 58 | apiId: 'api-id', 59 | authorizer: { 60 | jwt: { 61 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 62 | scopes: ['scope1', 'scope2'] 63 | } 64 | }, 65 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 66 | domainPrefix: 'id', 67 | http: { 68 | method: 'POST', 69 | path: '/my/path', 70 | protocol: 'HTTP/1.1', 71 | sourceIp: 'IP', 72 | userAgent: 'agent' 73 | }, 74 | requestId: 'id', 75 | routeKey: '$default', 76 | stage: '$default', 77 | time: '12/Mar/2020:19:03:58 +0000', 78 | timeEpoch: 1583348638390 79 | }, 80 | body: 'Hello from Lambda', 81 | pathParameters: { 'parameter1': 'value1' }, 82 | isBase64Encoded: false, 83 | stageVariables: { 'stageVariable1': 'value1', 'stageVariable2': 'value2' } 84 | }; 85 | 86 | // Construct dummy ALB event 87 | const albEvent = { 88 | "requestContext": { 89 | "elb": { 90 | "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" 91 | } 92 | }, 93 | "httpMethod": "GET", 94 | "path": "/", 95 | "queryStringParameters": { 96 | "myKey": "val2" 97 | }, 98 | "headers": { 99 | 'Header1': 'value1', 100 | 'Header2': 'value2' 101 | }, 102 | "isBase64Encoded": false, 103 | "body": 'Hello from Lambda' 104 | } 105 | 106 | // Construct dummy ALB event 107 | const albWithMultiValueHeadersEvent = { 108 | "requestContext": { 109 | "elb": { 110 | "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" 111 | } 112 | }, 113 | "httpMethod": "GET", 114 | "path": "/", 115 | "multiValueQueryStringParameters": { 116 | "myKey": ["val1", "val2"] 117 | }, 118 | "multiValueHeaders": { 119 | 'Header1': ['value1'], 120 | 'Header2': ['value2'] 121 | }, 122 | "isBase64Encoded": false, 123 | "body": 'Hello from Lambda' 124 | } 125 | 126 | 127 | it('parses chunked body on chunked transfer-encoding on v1Event', () => { 128 | const chunkedBody = '7\r\nCombine\r\n4\r\nThis\r\n4\r\nText\r\n0\r\n\r\n'; 129 | const response = Response.from({ 130 | body: chunkedBody, 131 | headers: { 'transfer-encoding': 'chunked'}, 132 | statusCode: 200 133 | }) 134 | expect(formatResponse(v1Event, response, {}).body).to.eql('CombineThisText'); 135 | }); 136 | 137 | it('parses chunked body on res.chunkedEncoding on v1Event', () => { 138 | const chunkedBody = '7\r\nCombine\r\n4\r\nThis\r\n4\r\nText\r\n0\r\n\r\n'; 139 | const response = Response.from({ 140 | body: chunkedBody, 141 | statusCode: 200 142 | }) 143 | response.chunkedEncoding = true; 144 | 145 | expect(formatResponse(v1Event, response, {}).body).to.eql('CombineThisText'); 146 | 147 | }); 148 | 149 | it("parses chunked body on transfer-encoding on v2Event", () => { 150 | const chunkedBody = '7\r\nCombine\r\n4\r\nThis\r\n4\r\nText\r\n0\r\n\r\n'; 151 | const response = Response.from({ 152 | body: chunkedBody, 153 | headers: { 'transfer-encoding': 'chunked'}, 154 | statusCode: 200 155 | }) 156 | expect(formatResponse(v2Event, response, {}).body).to.eql('CombineThisText'); 157 | 158 | }); 159 | 160 | it("parses chunked body on res.chunkedEncoding on v2Event", () => { 161 | const chunkedBody = '7\r\nCombine\r\n4\r\nThis\r\n4\r\nText\r\n0\r\n\r\n'; 162 | const response = Response.from({ 163 | body: chunkedBody, 164 | statusCode: 200 165 | }) 166 | response.chunkedEncoding = true; 167 | expect(formatResponse(v1Event, response, {}).body).to.eql('CombineThisText'); 168 | }); 169 | 170 | it("v2Event: return object contains cookies", () => { 171 | const response = new Response({}); 172 | response.headers['set-cookie'] = ['foo=bar', 'hail=hydra']; 173 | 174 | expect(formatResponse(v2Event, response, {}).cookies).to.exist; 175 | }); 176 | 177 | it("v2Event: cookies in return object is an array", () => { 178 | const response = new Response({}); 179 | response.headers["set-cookie"] = ["foo=bar", "hail=hydra"]; 180 | 181 | expect(Array.isArray(formatResponse(v2Event, response, {}).cookies)).to.be.true; 182 | }); 183 | 184 | it("v1Event: return object contains multiValueHeaders", () => { 185 | const response = new Response({}); 186 | response.headers["set-cookie"] = ["foo=bar", "hail=hydra"]; 187 | 188 | expect(formatResponse(v1Event, response, {}).multiValueHeaders).to.exist; 189 | }); 190 | 191 | it("v1Event: multiValueHeaders in return object is an object with value of each header an array", () => { 192 | const response = new Response({}); 193 | response.headers["set-cookie"] = ["foo=bar", "hail=hydra"]; 194 | 195 | expect(Array.isArray(formatResponse(v1Event, response, {}).multiValueHeaders['set-cookie'])).to.be 196 | .true; 197 | }); 198 | 199 | it("v2Event: cookies array in return object has correct values", () => { 200 | const response = new Response({}); 201 | response.headers["set-cookie"] = ["foo=bar", "hail=hydra"]; 202 | 203 | expect(formatResponse(v2Event, response, {}).cookies).to.eql([ 204 | "foo=bar", 205 | "hail=hydra", 206 | ]); 207 | }); 208 | 209 | it("v1Event: cookies array in multiValueHeaders object in return object has correct values", () => { 210 | const response = new Response({}); 211 | response.headers["set-cookie"] = ["foo=bar", "hail=hydra"]; 212 | 213 | expect(formatResponse(v1Event, response, {}).multiValueHeaders['set-cookie']).to.eql([ 214 | "foo=bar", 215 | "hail=hydra", 216 | ]); 217 | }); 218 | 219 | it('albEvent: handles a simple alb event', () => { 220 | const response = new Response({}); 221 | response.headers["set-cookie"] = "foo=bar"; 222 | response.headers["content-type"] = "application/json"; 223 | 224 | const result = formatResponse(albEvent, response, {}); 225 | expect(result.headers["set-cookie"]).to.eq("foo=bar"); 226 | expect(result.headers["content-type"]).to.eq("application/json"); 227 | }) 228 | 229 | it('albEvent: handles alb event when multi-value headers are set', () => { 230 | const response = new Response({}); 231 | response.headers["set-cookie"] = ["foo=bar", "hail=hydra"]; 232 | response.headers["content-type"] = "application/json"; 233 | 234 | const result = formatResponse(albWithMultiValueHeadersEvent, response, {}); 235 | expect(result.multiValueHeaders["set-cookie"]).to.deep.equal(["foo=bar", "hail=hydra"]); 236 | expect(result.multiValueHeaders["content-type"]).to.deep.equal(["application/json"]); 237 | }) 238 | 239 | }); 240 | -------------------------------------------------------------------------------- /test/koa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Koa = require('koa'), 4 | route = require('koa-route'), 5 | compress = require('koa-compress'), 6 | bodyparser = require('koa-bodyparser'), 7 | serve = require('koa-static'), 8 | Router = require('koa-router'), 9 | expect = require('chai').expect, 10 | zlib = require('zlib'), 11 | request = require('./util/request'); 12 | 13 | describe('koa', () => { 14 | let app; 15 | 16 | beforeEach(function() { 17 | app = new Koa(); 18 | }); 19 | 20 | it('basic middleware should set statusCode and default body', () => { 21 | app.use(async (ctx) => { 22 | ctx.status = 418; 23 | }); 24 | 25 | return request(app, { 26 | httpMethod: 'GET', 27 | path: '/' 28 | }) 29 | .then(response => { 30 | expect(response.statusCode).to.equal(418); 31 | expect(response.body).to.equal('I\'m a teapot') 32 | }); 33 | }); 34 | 35 | it('basic middleware should receive queryString', () => { 36 | app.use(async (ctx) => { 37 | ctx.body = ctx.query.x; 38 | }); 39 | 40 | return request(app, { 41 | httpMethod: 'GET', 42 | path: '/', 43 | queryStringParameters: { 44 | x: 'y' 45 | } 46 | }) 47 | .then(response => { 48 | expect(response.body).to.equal('y'); 49 | }); 50 | }); 51 | 52 | it('basic middleware should receive multi-value queryString', () => { 53 | app.use(async (ctx) => { 54 | ctx.body = ctx.query.x; 55 | }); 56 | 57 | return request(app, { 58 | httpMethod: 'GET', 59 | path: '/', 60 | queryStringParameters: { 61 | x: 'y' 62 | }, 63 | multiValueQueryStringParameters: { 64 | x: ['z', 'y'] 65 | } 66 | }) 67 | .then(response => { 68 | expect(response.body).to.equal('["z","y"]'); 69 | }); 70 | }); 71 | 72 | 73 | it('basic middleware should set statusCode and custom body', () => { 74 | app.use(async (ctx) => { 75 | ctx.status = 201; 76 | ctx.body = { foo: 'bar' }; 77 | }); 78 | 79 | return request(app, { 80 | httpMethod: 'GET', 81 | path: '/' 82 | }) 83 | .then(response => { 84 | expect(response.statusCode).to.equal(201); 85 | expect(response.body).to.equal('{"foo":"bar"}'); 86 | }); 87 | }); 88 | 89 | it('basic middleware should set headers', () => { 90 | app.use(async (ctx) => { 91 | ctx.body = { "test": "foo" }; 92 | ctx.set('X-Test-Header', 'foo'); 93 | }); 94 | 95 | return request(app, { 96 | httpMethod: 'GET', 97 | path: '/' 98 | }) 99 | .then(response => { 100 | expect(response.statusCode).to.equal(200); 101 | expect(response.headers).to.deep.equal({ 102 | 'content-length': '14', 103 | 'content-type': 'application/json; charset=utf-8', 104 | 'x-test-header': 'foo' 105 | }); 106 | }); 107 | }); 108 | 109 | it('basic middleware should get headers', () => { 110 | let headers; 111 | app.use(async (ctx) => { 112 | headers = ctx.request.headers; 113 | ctx.status = 204; 114 | }); 115 | 116 | return request(app, { 117 | httpMethod: 'GET', 118 | path: '/', 119 | headers: { 120 | 'X-Request-Id': 'abc' 121 | } 122 | }) 123 | .then(response => { 124 | expect(response.statusCode).to.equal(204); 125 | expect(headers['x-request-id']).to.equal('abc'); 126 | }); 127 | }); 128 | 129 | it('basic middleware should set string body', () => { 130 | app.use(async (ctx) => { 131 | ctx.body = 'Hello World'; 132 | }); 133 | 134 | return request(app, { 135 | httpMethod: 'GET', 136 | path: '/', 137 | headers: { 138 | 'X-Request-Id': 'abc' 139 | } 140 | }) 141 | .then(response => { 142 | expect(response.statusCode).to.equal(200); 143 | expect(response.body).to.equal('Hello World'); 144 | }); 145 | }); 146 | 147 | it('error middleware should set statusCode and default body', () => { 148 | app.use(async () => { 149 | throw new Error('hey man, nice shot'); 150 | }); 151 | return request(app, { 152 | httpMethod: 'GET', 153 | path: '/' 154 | }) 155 | .then(response => { 156 | expect(response.statusCode).to.equal(500); 157 | expect(response.body).to.equal('Internal Server Error') 158 | }); 159 | }); 160 | 161 | it('auth middleware should set statusCode 401', () => { 162 | app.use(async (ctx) => { 163 | ctx.throw(401, `Unauthorized: ${ctx.request.method} ${ctx.request.url}`); 164 | }); 165 | return request(app, { 166 | httpMethod: 'GET', 167 | path: '/' 168 | }) 169 | .then(response => { 170 | expect(response.statusCode).to.equal(401); 171 | }); 172 | }); 173 | 174 | 175 | describe('koa-route', () => { 176 | 177 | beforeEach(() => { 178 | app.use(route.get('/foo', async (ctx) => { 179 | ctx.body = 'foo'; 180 | })); 181 | app.use(route.get('/foo/:bar', async (ctx, bar) => { 182 | ctx.body = bar; 183 | })); 184 | app.use(route.post('/foo', async (ctx) => { 185 | ctx.status = 201; 186 | ctx.body = 'Thanks'; 187 | })); 188 | }); 189 | 190 | it('should get path information when it matches exactly', () => { 191 | return request(app, { 192 | httpMethod: 'GET', 193 | path: '/foo' 194 | }) 195 | .then(response => { 196 | expect(response.statusCode).to.equal(200); 197 | expect(response.body).to.equal('foo') 198 | }); 199 | }); 200 | 201 | it('should get path information when it matches with params', () => { 202 | return request(app, { 203 | httpMethod: 'GET', 204 | path: '/foo/baz' 205 | }) 206 | .then(response => { 207 | expect(response.statusCode).to.equal(200); 208 | expect(response.body).to.equal('baz') 209 | }); 210 | }); 211 | 212 | it('should get method information', () => { 213 | return request(app, { 214 | httpMethod: 'POST', 215 | path: '/foo' 216 | }) 217 | .then(response => { 218 | expect(response.statusCode).to.equal(201); 219 | expect(response.body).to.equal('Thanks') 220 | }); 221 | }); 222 | 223 | it('should allow 404s', () => { 224 | return request(app, { 225 | httpMethod: 'POST', 226 | path: '/missing' 227 | }) 228 | .then(response => { 229 | expect(response.statusCode).to.equal(404); 230 | }); 231 | }); 232 | }); 233 | 234 | describe('koa-router', function() { 235 | 236 | beforeEach(() => { 237 | const router = new Router(); 238 | 239 | router.use('/route', async (ctx, next) => { 240 | if (this.method === 'POST') { 241 | ctx.status = 404; 242 | } else { 243 | await next; 244 | } 245 | }); 246 | 247 | router.get('/', async (ctx) => { 248 | ctx.body = await Promise.resolve('hello'); 249 | }); 250 | 251 | app.use(router.routes()); 252 | app.use(router.allowedMethods()); 253 | }); 254 | 255 | it('should get when it matches', function() { 256 | return request(app, { 257 | httpMethod: 'GET', 258 | path: '/' 259 | }) 260 | .then((response) => { 261 | expect(response.statusCode).to.equal(200); 262 | expect(response.body).to.equal('hello'); 263 | }); 264 | }); 265 | 266 | it('should 404 when route does not match', function() { 267 | return request(app, { 268 | httpMethod: 'GET', 269 | path: '/missing' 270 | }) 271 | .then((response) => { 272 | expect(response.statusCode).to.equal(404); 273 | expect(response.headers).to.deep.equal({ 274 | 'content-length': '9', 275 | 'content-type': 'text/plain; charset=utf-8' 276 | }); 277 | }); 278 | }); 279 | }); 280 | 281 | describe('koa-bodyparser', () => { 282 | 283 | beforeEach(() => { 284 | app.use(bodyparser()); 285 | }); 286 | 287 | it('should parse json', () => { 288 | const body = `{"foo":"bar"}`; 289 | 290 | let actual; 291 | app.use(async(ctx) => { 292 | ctx.status = 204; 293 | ctx.body = {}; 294 | actual = ctx.request.body; 295 | }); 296 | return request(app, { 297 | httpMethod: 'GET', 298 | path: '/', 299 | headers: { 300 | 'Content-Type': 'application/json', 301 | 'Content-Length': body.length 302 | }, 303 | body 304 | }) 305 | .then(() => { 306 | expect(actual).to.deep.equal({ 307 | "foo": "bar" 308 | }); 309 | }); 310 | }); 311 | 312 | it('works with gzip (base64 encoded string)', () => { 313 | let actual; 314 | app.use(async (ctx) => { 315 | ctx.status = 204; 316 | ctx.body = {}; 317 | actual = ctx.request.body; 318 | }); 319 | 320 | return new Promise((resolve) => { 321 | zlib.gzip(`{"foo":"bar"}`, function (_, result) { 322 | resolve(result); 323 | }); 324 | }) 325 | .then((zipped) => { 326 | return request(app, { 327 | httpMethod: 'GET', 328 | path: '/', 329 | headers: { 330 | 'Content-Type': 'application/json', 331 | 'Content-Encoding': 'gzip', 332 | 'Content-Length': zipped.length, 333 | }, 334 | body: zipped.toString('base64'), 335 | isBase64Encoded: true 336 | }) 337 | .then(() => { 338 | expect(actual).to.deep.equal({ 339 | foo: "bar" 340 | }); 341 | }); 342 | }); 343 | }); 344 | 345 | it('can handle DELETE with no body', () => { 346 | let called; 347 | app.use(async (ctx) => { 348 | ctx.status = 204; 349 | called = true; 350 | }); 351 | return request(app, { 352 | httpMethod: 'DELETE', 353 | path: '/', 354 | headers: { 355 | 'Content-Type': 'application/json' 356 | } 357 | }) 358 | .then(() => { 359 | expect(called).to.equal(true); 360 | }); 361 | }); 362 | }); 363 | 364 | describe('koa-static', () => { 365 | 366 | beforeEach(() => { 367 | app.use(serve(__dirname)); 368 | }); 369 | 370 | it('should serve a text file', () => { 371 | return request(app, { 372 | httpMethod: 'GET', 373 | path: 'file.txt' 374 | }) 375 | .then((response) => { 376 | expect(response.body).to.equal('this is a test\n'); 377 | }); 378 | }); 379 | }); 380 | 381 | describe('koa-compress', () => { 382 | 383 | beforeEach(() => { 384 | app.use(compress({ 385 | threshold: 1 386 | })); 387 | app.use(async (ctx) => { 388 | ctx.body = 'this is a test'; 389 | }); 390 | }); 391 | 392 | it('should serve compressed text (base64 encoded)', () => { 393 | return request(app, { 394 | httpMethod: 'GET', 395 | path: '/', 396 | headers: { 397 | 'accept-encoding': 'deflate' 398 | } 399 | }) 400 | .then((response) => { 401 | const decoded = Buffer.from(response.body, 'base64'); 402 | const inflated = zlib.inflateSync(decoded); 403 | 404 | expect(inflated.toString()).to.equal('this is a test'); 405 | }); 406 | }); 407 | }); 408 | }); 409 | -------------------------------------------------------------------------------- /test/clean-up-event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cleanUpEvent = require('../lib/provider/aws/clean-up-event.js'); 4 | const expect = require('chai').expect; 5 | 6 | describe('clean up event', () => { 7 | it('should clean up api gateway payload format version 1.0 correctly', () => { 8 | // Construct dummy v1 event 9 | const v1Event = { 10 | version: '1.0', 11 | resource: '/my/path', 12 | path: '/my/path', 13 | httpMethod: 'GET', 14 | headers: { 15 | 'Header1': 'value1', 16 | 'Header2': 'value2' 17 | }, 18 | queryStringParameters: { parameter1: 'value1', parameter2: 'value' }, 19 | multiValueQueryStringParameters: { parameter1: ['value1', 'value2'], paramter2: ['value'] }, 20 | requestContext: { 21 | accountId: '123456789012', 22 | apiId: 'id', 23 | authorizer: { claims: null, scopes: null }, 24 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 25 | domainPrefix: 'id', 26 | extendedRequestId: 'request-id', 27 | httpMethod: 'GET', 28 | path: '/my/path', 29 | protocol: 'HTTP/1.1', 30 | requestId: 'id=', 31 | requestTime: '04/Mar/2020:19:15:17 +0000', 32 | requestTimeEpoch: 1583349317135, 33 | resourceId: null, 34 | resourcePath: '/my/path', 35 | stage: '$default' 36 | }, 37 | pathParameters: null, 38 | stageVariables: null, 39 | body: 'Hello from Lambda!', 40 | isBase64Encoded: true 41 | }; 42 | 43 | // Clean the event 44 | cleanUpEvent(v1Event, { basePath: '/my' }); 45 | 46 | expect(v1Event).to.eql({ 47 | version: '1.0', 48 | resource: '/my/path', 49 | path: '/path', 50 | httpMethod: 'GET', 51 | headers: { Header1: 'value1', Header2: 'value2' }, 52 | queryStringParameters: { parameter1: 'value1', parameter2: 'value' }, 53 | multiValueQueryStringParameters: { parameter1: ['value1', 'value2'], paramter2: ['value'] }, 54 | requestContext: { 55 | accountId: '123456789012', 56 | apiId: 'id', 57 | authorizer: { claims: null, scopes: null }, 58 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 59 | domainPrefix: 'id', 60 | extendedRequestId: 'request-id', 61 | httpMethod: 'GET', 62 | path: '/my/path', 63 | protocol: 'HTTP/1.1', 64 | requestId: 'id=', 65 | requestTime: '04/Mar/2020:19:15:17 +0000', 66 | requestTimeEpoch: 1583349317135, 67 | resourceId: null, 68 | resourcePath: '/my/path', 69 | stage: '$default', 70 | identity: {} 71 | }, 72 | pathParameters: null, 73 | stageVariables: null, 74 | body: 'Hello from Lambda!', 75 | isBase64Encoded: true 76 | }); 77 | }); 78 | 79 | it('should clean up api gateway payload format version 2.0 correctly', () => { 80 | // Construct dummy v2 event 81 | const v2Event = { 82 | version: '2.0', 83 | routeKey: '$default', 84 | rawPath: '/my/path', 85 | rawQueryString: 'parameter1=value1¶meter1=value2¶meter2=value', 86 | cookies: ['cookie1', 'cookie2'], 87 | headers: { 88 | 'Header1': 'value1', 89 | 'Header2': 'value2' 90 | }, 91 | queryStringParameters: { parameter1: 'value1,value2', parameter2: 'value' }, 92 | requestContext: { 93 | accountId: '123456789012', 94 | apiId: 'api-id', 95 | authorizer: { 96 | jwt: { 97 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 98 | scopes: ['scope1', 'scope2'] 99 | } 100 | }, 101 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 102 | domainPrefix: 'id', 103 | http: { 104 | method: 'POST', 105 | path: '/my/path', 106 | protocol: 'HTTP/1.1', 107 | sourceIp: 'IP', 108 | userAgent: 'agent' 109 | }, 110 | requestId: 'id', 111 | routeKey: '$default', 112 | stage: '$default', 113 | time: '12/Mar/2020:19:03:58 +0000', 114 | timeEpoch: 1583348638390 115 | }, 116 | body: 'Hello from Lambda', 117 | pathParameters: { 'parameter1': 'value1' }, 118 | isBase64Encoded: false, 119 | stageVariables: { 'stageVariable1': 'value1', 'stageVariable2': 'value2' } 120 | }; 121 | 122 | // Clean the event 123 | cleanUpEvent(v2Event, { basePath: '/my' }); 124 | 125 | expect(v2Event).to.eql({ 126 | version: '2.0', 127 | routeKey: '$default', 128 | rawPath: '/path', 129 | rawQueryString: 'parameter1=value1¶meter1=value2¶meter2=value', 130 | cookies: ['cookie1', 'cookie2'], 131 | headers: { Header1: 'value1', Header2: 'value2' }, 132 | queryStringParameters: { parameter1: 'value1,value2', parameter2: 'value' }, 133 | requestContext: { 134 | accountId: '123456789012', 135 | apiId: 'api-id', 136 | authorizer: { 137 | jwt: { 138 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 139 | scopes: ['scope1', 'scope2'] 140 | } 141 | }, 142 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 143 | domainPrefix: 'id', 144 | http: { 145 | method: 'POST', 146 | path: '/my/path', 147 | protocol: 'HTTP/1.1', 148 | sourceIp: 'IP', 149 | userAgent: 'agent' 150 | }, 151 | requestId: 'id', 152 | routeKey: '$default', 153 | stage: '$default', 154 | time: '12/Mar/2020:19:03:58 +0000', 155 | timeEpoch: 1583348638390 156 | }, 157 | body: 'Hello from Lambda', 158 | pathParameters: { parameter1: 'value1' }, 159 | isBase64Encoded: false, 160 | stageVariables: { stageVariable1: 'value1', stageVariable2: 'value2' } 161 | }); 162 | 163 | }); 164 | 165 | it('should properly urldecode ELB payload query params', () => { 166 | // Construct dummy v2 event 167 | const v2Event = { 168 | version: '2.0', 169 | routeKey: '$default', 170 | rawPath: '/my/path', 171 | rawQueryString: 'parameter%231=value%231¶meter%231=value%232¶meter2=value¶meter3=hello+world¶meter4=%%TEST%%', 172 | cookies: ['cookie1', 'cookie2'], 173 | headers: { 174 | 'Header1': 'value1', 175 | 'Header2': 'value2' 176 | }, 177 | queryStringParameters: { 'parameter%231': 'value%231,value%232', 'parameter2': 'value', 'parameter3': 'hello+world', 'parameter4': '%%TEST%%' }, 178 | multiValueQueryStringParameters: { 'parameter%231': ['value%231', 'value%232'], 'parameter2': ['value'], 'parameter3': ['hello+world'], 'parameter4': ['%%TEST%%'] }, 179 | requestContext: { 180 | accountId: '123456789012', 181 | apiId: 'api-id', 182 | authorizer: { 183 | jwt: { 184 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 185 | scopes: ['scope1', 'scope2'] 186 | } 187 | }, 188 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 189 | domainPrefix: 'id', 190 | http: { 191 | method: 'POST', 192 | path: '/my/path', 193 | protocol: 'HTTP/1.1', 194 | sourceIp: 'IP', 195 | userAgent: 'agent' 196 | }, 197 | requestId: 'id', 198 | routeKey: '$default', 199 | stage: '$default', 200 | time: '12/Mar/2020:19:03:58 +0000', 201 | timeEpoch: 1583348638390, 202 | elb: { 203 | targetGroupArn: "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" 204 | } 205 | }, 206 | body: 'Hello from Lambda', 207 | pathParameters: { 'parameter1': 'value1' }, 208 | isBase64Encoded: false, 209 | stageVariables: { 'stageVariable1': 'value1', 'stageVariable2': 'value2' } 210 | }; 211 | 212 | // Clean the event 213 | cleanUpEvent(v2Event, { basePath: '/my' }); 214 | 215 | expect(v2Event).to.eql({ 216 | version: '2.0', 217 | routeKey: '$default', 218 | rawPath: '/path', 219 | rawQueryString: 'parameter%231=value%231¶meter%231=value%232¶meter2=value¶meter3=hello+world¶meter4=%%TEST%%', 220 | cookies: ['cookie1', 'cookie2'], 221 | headers: { Header1: 'value1', Header2: 'value2' }, 222 | queryStringParameters: { 'parameter#1': 'value#1,value#2', 'parameter2': 'value', 'parameter3': 'hello world', 'parameter4': '%%TEST%%' }, 223 | multiValueQueryStringParameters: { 'parameter#1': ['value#1', 'value#2'], 'parameter2': ['value'], 'parameter3': ['hello world'], 'parameter4': ['%%TEST%%'] }, 224 | requestContext: { 225 | accountId: '123456789012', 226 | apiId: 'api-id', 227 | authorizer: { 228 | jwt: { 229 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 230 | scopes: ['scope1', 'scope2'] 231 | } 232 | }, 233 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 234 | domainPrefix: 'id', 235 | http: { 236 | method: 'POST', 237 | path: '/my/path', 238 | protocol: 'HTTP/1.1', 239 | sourceIp: 'IP', 240 | userAgent: 'agent' 241 | }, 242 | requestId: 'id', 243 | routeKey: '$default', 244 | stage: '$default', 245 | time: '12/Mar/2020:19:03:58 +0000', 246 | timeEpoch: 1583348638390, 247 | elb: { 248 | targetGroupArn: "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" 249 | } 250 | }, 251 | body: 'Hello from Lambda', 252 | pathParameters: { parameter1: 'value1' }, 253 | isBase64Encoded: false, 254 | stageVariables: { stageVariable1: 'value1', stageVariable2: 'value2' } 255 | }); 256 | 257 | }); 258 | 259 | it('should not urldecode query params for non ELB payloads', () => { 260 | // Construct dummy v2 event 261 | const v2Event = { 262 | version: '2.0', 263 | routeKey: '$default', 264 | rawPath: '/my/path', 265 | rawQueryString: 'parameter%231=value%231¶meter%231=value%232¶meter2=value', 266 | cookies: ['cookie1', 'cookie2'], 267 | headers: { 268 | 'Header1': 'value1', 269 | 'Header2': 'value2' 270 | }, 271 | queryStringParameters: { 'parameter%231': 'value%231,value%232', 'parameter2': 'value' }, 272 | requestContext: { 273 | accountId: '123456789012', 274 | apiId: 'api-id', 275 | authorizer: { 276 | jwt: { 277 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 278 | scopes: ['scope1', 'scope2'] 279 | } 280 | }, 281 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 282 | domainPrefix: 'id', 283 | http: { 284 | method: 'POST', 285 | path: '/my/path', 286 | protocol: 'HTTP/1.1', 287 | sourceIp: 'IP', 288 | userAgent: 'agent' 289 | }, 290 | requestId: 'id', 291 | routeKey: '$default', 292 | stage: '$default', 293 | time: '12/Mar/2020:19:03:58 +0000', 294 | timeEpoch: 1583348638390 295 | }, 296 | body: 'Hello from Lambda', 297 | pathParameters: { 'parameter1': 'value1' }, 298 | isBase64Encoded: false, 299 | stageVariables: { 'stageVariable1': 'value1', 'stageVariable2': 'value2' } 300 | }; 301 | 302 | // Clean the event 303 | cleanUpEvent(v2Event, { basePath: '/my' }); 304 | 305 | expect(v2Event).to.eql({ 306 | version: '2.0', 307 | routeKey: '$default', 308 | rawPath: '/path', 309 | rawQueryString: 'parameter%231=value%231¶meter%231=value%232¶meter2=value', 310 | cookies: ['cookie1', 'cookie2'], 311 | headers: { Header1: 'value1', Header2: 'value2' }, 312 | queryStringParameters: { 'parameter%231': 'value%231,value%232', 'parameter2': 'value' }, 313 | requestContext: { 314 | accountId: '123456789012', 315 | apiId: 'api-id', 316 | authorizer: { 317 | jwt: { 318 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 319 | scopes: ['scope1', 'scope2'] 320 | } 321 | }, 322 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 323 | domainPrefix: 'id', 324 | http: { 325 | method: 'POST', 326 | path: '/my/path', 327 | protocol: 'HTTP/1.1', 328 | sourceIp: 'IP', 329 | userAgent: 'agent' 330 | }, 331 | requestId: 'id', 332 | routeKey: '$default', 333 | stage: '$default', 334 | time: '12/Mar/2020:19:03:58 +0000', 335 | timeEpoch: 1583348638390 336 | }, 337 | body: 'Hello from Lambda', 338 | pathParameters: { parameter1: 'value1' }, 339 | isBase64Encoded: false, 340 | stageVariables: { stageVariable1: 'value1', stageVariable2: 'value2' } 341 | }); 342 | 343 | }); 344 | }); 345 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getStream = require('get-stream'); 4 | const onHeaders = require('on-headers'); 5 | const onFinished = require('on-finished'); 6 | const chai = require('chai'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | const { expect } = chai; 12 | 13 | const serverless = require('../serverless-http'); 14 | 15 | describe('spec', () => { 16 | it('should throw when a non express/koa style object is passed', () => { 17 | expect(() => serverless({})).to.throw(Error); 18 | }); 19 | 20 | it('should set a default event', async () => { 21 | let request; 22 | 23 | const handler = serverless((req, res) => { 24 | request = req; 25 | res.end(''); 26 | }); 27 | 28 | await expect(handler(null)).to.be.fulfilled; 29 | expect(request).to.be.an('Object'); 30 | }); 31 | 32 | it('should trigger on-headers for res', async () => { 33 | let called = false; 34 | const handler = serverless((req, res) => { 35 | onHeaders(res, () => { 36 | called = true; 37 | }); 38 | res.end(''); 39 | }); 40 | 41 | await expect(handler(null)).to.be.fulfilled; 42 | expect(called).to.be.true; 43 | }); 44 | 45 | it('should trigger on-finished for res', async () => { 46 | return new Promise(resolve => { 47 | const handler = serverless((req, res) => { 48 | onFinished(res, () => { 49 | resolve(); 50 | }); 51 | res.end(''); 52 | }); 53 | 54 | handler(null); 55 | }); 56 | }); 57 | 58 | it('should trigger on-finished for req', async () => { 59 | let called = false; 60 | const handler = serverless((req, res) => { 61 | onFinished(req, () => { 62 | called = true; 63 | res.end(''); 64 | }); 65 | }); 66 | 67 | await expect(handler(null)).to.be.fulfilled; 68 | expect(called).to.be.true; 69 | }); 70 | 71 | it('should set default requestId', async () => { 72 | let called; 73 | 74 | const handler = serverless((req, res) => { 75 | onHeaders(res, () => { 76 | called = req; 77 | }); 78 | res.end(''); 79 | }); 80 | 81 | await handler({ requestContext: { requestId: 'foo' } }); 82 | expect(!!called).to.be.true; 83 | expect(called.headers['x-request-id']).to.eql('foo'); 84 | }); 85 | 86 | it('should set custom requestId', async () => { 87 | let called; 88 | const handler = serverless((req, res) => { 89 | onHeaders(res, () => { 90 | called = req; 91 | }); 92 | res.end(''); 93 | }, { requestId: 'Custom-Request-ID' }); 94 | 95 | await handler({ requestContext: { requestId: 'bar' } }); 96 | expect(!!called).to.be.true; 97 | expect(called.headers['custom-request-id']).to.eql('bar'); 98 | }); 99 | 100 | it('should use requestPath when available', async () => { 101 | let url; 102 | const handler = serverless((req, res) => { 103 | url = req.url; 104 | res.end(''); 105 | }); 106 | 107 | await handler({ requestPath: '/different', requestContext: { requestId: 'bar' } }); 108 | expect(url).to.deep.equal('/different'); 109 | }); 110 | 111 | it('should keep existing requestId', async () => { 112 | let called; 113 | const handler = serverless((req, res) => { 114 | onHeaders(res, () => { 115 | called = req; 116 | }); 117 | res.end(''); 118 | }, { requestId: 'Custom-Request-ID' }); 119 | 120 | await handler({ headers: { 'custom-request-id': 'abc' }, requestContext: { requestId: 'bar' } }); 121 | expect(!!called).to.be.true; 122 | expect(called.headers['custom-request-id']).to.eql('abc'); 123 | }); 124 | 125 | it('should not set request id when disabled', async () => { 126 | let called; 127 | const handler = serverless((req, res) => { 128 | onHeaders(res, () => { 129 | called = req; 130 | }); 131 | res.end(''); 132 | }, { requestId: false }); 133 | 134 | await handler({ requestContext: { requestId: 'bar' } }); 135 | expect(!!called).to.be.true; 136 | expect(called.headers['x-request-id']).to.be.undefined; 137 | }); 138 | 139 | it('should set request context on request', async () => { 140 | let called; 141 | const handler = serverless((req, res) => { 142 | onHeaders(res, () => { 143 | called = req; 144 | }); 145 | res.end(''); 146 | }); 147 | const requestContext = {}; 148 | await handler({ requestContext }); 149 | expect(!!called).to.be.true; 150 | expect(called.requestContext).to.equal(requestContext); 151 | }); 152 | 153 | it('should support transforming the request', async () => { 154 | let request; 155 | const event = {} 156 | const context = {} 157 | 158 | const handler = serverless((req, res) => { 159 | request = req; 160 | res.end(''); 161 | }, { 162 | request: (req, evt, ctx) => { 163 | req.event = evt; 164 | req.context = ctx; 165 | } 166 | }); 167 | 168 | await handler(event, context); 169 | expect(request.event).to.equal(event); 170 | expect(request.context).to.equal(context); 171 | }); 172 | 173 | it('should transform different payload format requests the same', async () => { 174 | let requestV1; 175 | let requestV2; 176 | const context = {} 177 | 178 | const handler1 = serverless((req, res) => { 179 | requestV1 = req; 180 | res.end(''); 181 | }, { 182 | request: (req, evt, ctx) => { 183 | req.event = evt; 184 | req.context = ctx; 185 | } 186 | }); 187 | 188 | const handler2 = serverless((req, res) => { 189 | requestV2 = req; 190 | res.end(''); 191 | }, { 192 | request: (req, evt, ctx) => { 193 | req.event = evt; 194 | req.context = ctx; 195 | } 196 | }); 197 | 198 | const v1Event = { 199 | version: '1.0', 200 | resource: '/my/path', 201 | path: '/my/path', 202 | httpMethod: 'GET', 203 | headers: { 204 | 'cookie': "cookie1; cookie2", 205 | 'Header1': 'value1', 206 | 'Header2': 'value2' 207 | }, 208 | queryStringParameters: { parameter1: 'value1', parameter2: 'value' }, 209 | multiValueQueryStringParameters: { parameter1: ['value1', 'value2'], parameter2: ['value'] }, 210 | requestContext: { 211 | accountId: '123456789012', 212 | apiId: 'id', 213 | authorizer: { 214 | jwt: { 215 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 216 | scopes: ['scope1', 'scope2'] 217 | } 218 | }, 219 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 220 | domainPrefix: 'id', 221 | extendedRequestId: 'request-id', 222 | httpMethod: 'GET', 223 | path: '/my/path', 224 | protocol: 'HTTP/1.1', 225 | requestId: 'x-request-id', 226 | requestTime: '04/Mar/2020:19:15:17 +0000', 227 | requestTimeEpoch: 1583349317135, 228 | resourceId: null, 229 | resourcePath: '/my/path', 230 | stage: '$default', 231 | identity: { 232 | accessKey: null, 233 | accountId: null, 234 | caller: null, 235 | cognitoAuthenticationProvider: null, 236 | cognitoAuthenticationType: null, 237 | cognitoIdentityId: null, 238 | cognitoIdentityPoolId: null, 239 | principalOrgId: null, 240 | sourceIp: 'IP', 241 | user: null, 242 | userAgent: 'user-agent', 243 | userArn: null 244 | }, 245 | }, 246 | pathParameters: null, 247 | stageVariables: null, 248 | body: 'Hello from Lambda', 249 | isBase64Encoded: false 250 | }; 251 | 252 | const v2Event = { 253 | version: '2.0', 254 | routeKey: '$default', 255 | rawPath: '/my/path', 256 | rawQueryString: 'parameter1=value1¶meter1=value2¶meter2=value', 257 | cookies: ['cookie1', 'cookie2'], 258 | headers: { 259 | 'Header1': 'value1', 260 | 'Header2': 'value2' 261 | }, 262 | queryStringParameters: { parameter1: 'value1,value2', parameter2: 'value' }, 263 | requestContext: { 264 | accountId: '123456789012', 265 | apiId: 'id', 266 | authorizer: { 267 | jwt: { 268 | claims: { 'claim1': 'value1', 'claim2': 'value2' }, 269 | scopes: ['scope1', 'scope2'] 270 | } 271 | }, 272 | domainName: 'id.execute-api.us-east-1.amazonaws.com', 273 | domainPrefix: 'id', 274 | http: { 275 | method: 'GET', 276 | path: '/my/path', 277 | protocol: 'HTTP/1.1', 278 | sourceIp: 'IP', 279 | userAgent: 'agent' 280 | }, 281 | requestId: 'x-request-id', 282 | routeKey: '$default', 283 | stage: '$default', 284 | time: '12/Mar/2020:19:03:58 +0000', 285 | timeEpoch: 1583348638390 286 | }, 287 | body: 'Hello from Lambda', 288 | pathParameters: { 'parameter1': 'value1' }, 289 | isBase64Encoded: false, 290 | stageVariables: { 'stageVariable1': 'value1', 'stageVariable2': 'value2' } 291 | }; 292 | 293 | await handler1(v1Event, context); 294 | await handler2(v2Event, context); 295 | 296 | //Remove the event and context objects as those will be different 297 | delete requestV1.event; 298 | delete requestV2.event; 299 | delete requestV1.requestContext; 300 | delete requestV2.requestContext; 301 | delete requestV1.apiGateway; 302 | delete requestV2.apiGateway; 303 | 304 | expect(JSON.stringify(requestV1)).to.equal(JSON.stringify(requestV2)); 305 | }); 306 | 307 | it('should support transforming the response', async () => { 308 | const handler = serverless((req, res) => { 309 | res.end(''); 310 | }, { 311 | response: (res) => { 312 | res.statusCode = 201; 313 | res.headers['foo'] = 'bar'; 314 | res.setHeader('bar', 'baz'); 315 | } 316 | }); 317 | 318 | const obj = await handler({}); 319 | expect(obj.statusCode).to.equal(201); 320 | expect(obj.headers).to.have.property('foo', 'bar'); 321 | expect(obj.headers).to.have.property('bar', 'baz'); 322 | }); 323 | 324 | it('should handle unicode when inferring content-length', async () => { 325 | const body = `{"foo":"অ"}`; 326 | 327 | let length; 328 | 329 | const handler = serverless((req, res) => { 330 | length = req.headers['content-length']; 331 | res.end(''); 332 | }); 333 | 334 | await handler({ body }); 335 | expect(length).to.equal(13); 336 | }); 337 | 338 | it('should add apiGateway with event/context to req', async () => { 339 | const body = `{"foo":"অ"}`; 340 | 341 | let captured; 342 | 343 | const handler = serverless((req, res) => { 344 | captured = req; 345 | res.end(''); 346 | }); 347 | 348 | const event = { body }; 349 | const context = { test: true }; 350 | 351 | await handler(event, context); 352 | 353 | expect(captured.apiGateway).to.deep.equal({ event, context }); 354 | }); 355 | 356 | it('should throw if event.body is a number', async () => { 357 | const body = 42; 358 | 359 | const handler = serverless((req, res) => { 360 | res.end(''); 361 | }); 362 | 363 | await expect(handler({ body })) 364 | .to.be.rejectedWith(Error, 'Unexpected event.body type: number'); 365 | }); 366 | 367 | it('should stringify if event.body is an object', async () => { 368 | const body = { foo: 'bar' }; 369 | const expected = Buffer.from(JSON.stringify(body)); 370 | let actual; 371 | const handler = serverless((req, res) => { 372 | actual = req.body; 373 | res.end(''); 374 | }); 375 | 376 | await handler({ body }); 377 | expect(actual).to.deep.equal(expected); 378 | }); 379 | 380 | it('should accept a Buffer body', async () => { 381 | const body = Buffer.from('hello world'); 382 | 383 | const handler = serverless((req, res) => { 384 | getStream(req).then(str => { 385 | res.end(str); 386 | }); 387 | }); 388 | 389 | const res = await handler({ body }); 390 | expect(res) 391 | .to.be.an('Object') 392 | .with.a.property('body', 'hello world'); 393 | }); 394 | 395 | it('should stringify an Object body if content-type is json', async () => { 396 | const body = { foo: 'bar' }; 397 | const headers = { 'content-type': 'application/json' }; 398 | 399 | const handler = serverless((req, res) => { 400 | getStream(req).then(str => { 401 | res.end(str); 402 | }); 403 | }); 404 | 405 | const res = await handler({ body, headers }); 406 | expect(res) 407 | .to.be.an('Object') 408 | .with.a.property('body', '{"foo":"bar"}'); 409 | }); 410 | 411 | it('should support returning a promise that rejects and not need a callback', async () => { 412 | const handler = serverless(() => { 413 | throw new Error('test'); 414 | }); 415 | 416 | await expect(handler({})).to.be.rejectedWith('test'); 417 | }); 418 | 419 | it('should supporte res.writeHead', async () => { 420 | const handler = serverless((req, res) => { 421 | res.writeHead(301, { Location: '/foo', }); 422 | res.end(); 423 | }); 424 | 425 | const response = await handler({}); 426 | expect(response.statusCode).to.equal(301); 427 | expect(response.headers).to.have.property('location', '/foo'); 428 | }); 429 | 430 | it('should support UInt8Array data', async () => { 431 | const expected = "hello"; 432 | 433 | const uint8Array = Uint8Array.from( 434 | Array.from(expected).map( 435 | ch => ch.charCodeAt(0) 436 | ) 437 | ); 438 | 439 | const handler = serverless((req, res) => { 440 | res.end(uint8Array); 441 | }); 442 | 443 | const response = await handler({}); 444 | 445 | expect(response.body).to.equal(expected); 446 | }); 447 | 448 | }); 449 | --------------------------------------------------------------------------------