Skip to content

Commit

Permalink
feat: add in support for vpc lattice
Browse files Browse the repository at this point in the history
  • Loading branch information
willfarrell committed May 31, 2023
1 parent a01d8fc commit 06694fc
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 14 deletions.
1 change: 1 addition & 0 deletions packages/http-content-encoding/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const httpContentEncodingMiddleware = (opts) => {
for await (const chunk of stream) {
chunks.push(chunk)
}
// TODO update to btoa if faster
const body = Buffer.concat(chunks).toString('base64')

// Only apply encoding if it's smaller
Expand Down
41 changes: 39 additions & 2 deletions packages/http-event-normalizer/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test('It should do nothing if not HTTP event', async (t) => {
}
})

test('It should default queryStringParameters', async (t) => {
test('It should default queryStringParameters with REST API', async (t) => {
const event = {
httpMethod: 'GET'
}
Expand All @@ -60,6 +60,43 @@ test('It should default queryStringParameters with HTTP API', async (t) => {
t.deepEqual(normalizedEvent.queryStringParameters, {})
})

test('It should default queryStringParameters with VPC Lattice', async (t) => {
const event = {
method: 'GET'
}

const handler = middy((event) => event).use(httpEventNormalizer())
const normalizedEvent = await handler(event, context)

t.deepEqual(normalizedEvent.queryStringParameters, {})
})

test('It should set queryStringParameters with VPC Lattice', async (t) => {
const event = {
method: 'GET',
query_string_parameters: {
foo: 'bar'
}
}

const handler = middy((event) => event).use(httpEventNormalizer())
const normalizedEvent = await handler(event, context)

t.deepEqual(normalizedEvent.queryStringParameters, { foo: 'bar' })
})

test('It should set isBase64Encoded with VPC Lattice', async (t) => {
const event = {
method: 'GET',
is_base64_encoded: false
}

const handler = middy((event) => event).use(httpEventNormalizer())
const normalizedEvent = await handler(event, context)

t.is(normalizedEvent.isBase64Encoded, false)
})

test('It should default multiValueQueryStringParameters', async (t) => {
const event = {
httpMethod: 'GET'
Expand All @@ -71,7 +108,7 @@ test('It should default multiValueQueryStringParameters', async (t) => {
t.deepEqual(normalizedEvent.multiValueQueryStringParameters, {})
})

test('It should default pathParameters', async (t) => {
test('It should default pathParameters with REST API', async (t) => {
const event = {
httpMethod: 'GET'
}
Expand Down
3 changes: 2 additions & 1 deletion packages/http-event-normalizer/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
APIGatewayProxyEventMultiValueQueryStringParameters,
APIGatewayProxyEventPathParameters,
APIGatewayProxyEventQueryStringParameters
// TODO add in VPC Lattice event
} from 'aws-lambda'

export type Event = APIGatewayEvent & {
Expand All @@ -12,6 +13,6 @@ export type Event = APIGatewayEvent & {
queryStringParameters: APIGatewayProxyEventQueryStringParameters
}

declare function httpEventNormalizer (): middy.MiddlewareObj<Event>
declare function httpEventNormalizer(): middy.MiddlewareObj<Event>

export default httpEventNormalizer
23 changes: 18 additions & 5 deletions packages/http-event-normalizer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,41 @@ const httpEventNormalizerMiddleware = () => {
const httpEventNormalizerMiddlewareBefore = async (request) => {
const { event } = request

const version = event.version ?? '1.0'
const version = pickVersion(event)
const isHttpEvent = isVersionHttpEvent[version]?.(event)
if (!isHttpEvent) {
throw new Error('[http-event-normalizer] Unknown http event format')
}
// VPC Lattice is an http event, however uses a different notation
// - query_string_parameters
// - is_base64_encoded

// event.headers ??= {} // Will always have at least one header
event.pathParameters ??= {}
event.queryStringParameters ??= {}
if (version === '1.0') {
event.multiValueQueryStringParameters ??= {}
} else if (version === 'vpc') {
event.queryStringParameters = event.query_string_parameters
event.isBase64Encoded = event.is_base64_encoded
}

// event.headers ??= {} // Will always have at least one header
event.pathParameters ??= {}
event.queryStringParameters ??= {}
}

return {
before: httpEventNormalizerMiddlewareBefore
}
}

const pickVersion = (event) => {
// '1.0' is a safer default
return event.version ?? (event.method ? 'vpc' : '1.0')
}

const isVersionHttpEvent = {
'1.0': (event) => typeof event.httpMethod !== 'undefined',
'2.0': (event) => typeof event.requestContext.http.method !== 'undefined'
'2.0': (event) => typeof event.requestContext.http.method !== 'undefined',
vpc: (event) => typeof event.method !== 'undefined'
}

export default httpEventNormalizerMiddleware
32 changes: 32 additions & 0 deletions packages/http-router/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,22 @@ test('It should route to a dynamic ANY method', async (t) => {
})

// event versions
test('It should route to a REST v1 event', async (t) => {
const event = {
httpMethod: 'GET',
path: '/'
}
const handler = httpRouter([
{
method: 'GET',
path: '/',
handler: () => true
}
])
const response = await handler(event, context)
t.true(response)
})

test('It should route to a v2 event', async (t) => {
const event = {
version: '2.0',
Expand All @@ -287,6 +303,22 @@ test('It should route to a v2 event', async (t) => {
t.true(response)
})

test('It should route to a VPC Lattice event', async (t) => {
const event = {
method: 'GET',
raw_path: '/'
}
const handler = httpRouter([
{
method: 'GET',
path: '/',
handler: () => true
}
])
const response = await handler(event, context)
t.true(response)
})

// with middleware
test('It should run middleware that are part of route handler', async (t) => {
const event = {
Expand Down
11 changes: 10 additions & 1 deletion packages/http-router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const httpRouteHandler = (routes) => {
}

return (event, context, abort) => {
const { method, path } = getVersionRoute[event.version ?? '1.0']?.(event)
const { method, path } = getVersionRoute[pickVersion(event)]?.(event)
if (!method) {
throw new Error('[http-router] Unknown http event format')
}
Expand Down Expand Up @@ -92,6 +92,11 @@ const attachDynamicRoute = (method, path, handler, routesType) => {
routesType[method].push({ path, handler })
}

const pickVersion = (event) => {
// '1.0' is a safer default
return event.version ?? (event.method ? 'vpc' : '1.0')
}

const getVersionRoute = {
'1.0': (event) => ({
method: event.httpMethod,
Expand All @@ -100,6 +105,10 @@ const getVersionRoute = {
'2.0': (event) => ({
method: event.requestContext.http.method,
path: event.requestContext.http.path
}),
vpc: (event) => ({
method: event.method,
path: event.raw_path.split('?')[0]
})
}

Expand Down
84 changes: 84 additions & 0 deletions website/docs/events/vpc-lattice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: VPC Lattice
---

:::caution

This page is a work in progress. If you want to help us to make this page better, please consider contributing on GitHub.

:::

We recommend using `@middy/http-event-normalizer` if you place to use any of the following: `@middy/http-json-body-parser`, `@middy/http-multipart-body-parser`, `@middy/http-urlencode-body-parser`, `@middy/http-partial-response`

## AWS Documentation

- [Using AWS Lambda with Amazon VPC Lattice](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html)

## Example

```javascript
import middy from '@middy/core'
import errorLoggerMiddleware from '@middy/error-logger'
import inputOutputLoggerMiddleware from '@middy/input-output-logger'
import httpContentNegotiationMiddleware from '@middy/http-content-negotiation'
import httpContentEncodingMiddleware from '@middy/http-content-encoding'
import httpCorsMiddleware from '@middy/http-cors'
import httpErrorHandlerMiddleware from '@middy/http-error-handler'
import httpEventNormalizerMiddleware from '@middy/http-event-normalizer' // required
import httpHeaderNormalizerMiddleware from '@middy/http-header-normalizer'
import httpJsonBodyParserMiddleware from '@middy/http-json-body-parser'
import httpMultipartBodyParserMiddleware from '@middy/http-multipart-body-parser'
import httpPartialResponseMiddleware from '@middy/http-partial-response'
import httpResponseSerializerMiddleware from '@middy/http-response-serializer'
import httpSecurityHeadersMiddleware from '@middy/http-security-headers'
import httpUrlencodeBodyParserMiddleware from '@middy/http-urlencode-body-parser'
import httpUrlencodePathParametersParserMiddleware from '@middy/http-urlencode-path-parser'
import validatorMiddleware from 'validator' // or `middy-ajv`
import warmupMiddleware from 'warmup'

import eventSchema from './eventSchema.json' assert { type: 'json' }
import responseSchema from './responseSchema.json' assert { type: 'json' }

export const handler = middy({
timeoutEarlyResponse: () => {
return {
statusCode: 408
}
}
})
.use(warmupMiddleware())
.use(httpEventNormalizerMiddleware())
.use(httpHeaderNormalizerMiddleware())
.use(
httpContentNegotiationMiddleware({
availableLanguages: ['en-CA', 'fr-CA'],
availableMediaTypes: ['application/json']
})
)
.use(httpUrlencodePathParametersParserMiddleware())
// Start oneOf
.use(httpUrlencodeBodyParserMiddleware())
.use(httpJsonBodyParserMiddleware())
.use(httpMultipartBodyParserMiddleware())
// End oneOf
.use(httpSecurityHeadersMiddleware())
.use(httpCorsMiddleware())
.use(httpContentEncodingMiddleware())
.use(
httpResponseSerializerMiddleware({
serializers: [
{
regex: /^application\/json$/,
serializer: ({ body }) => JSON.stringify(body)
}
],
default: 'application/json'
})
)
.use(httpPartialResponseMiddleware())
.use(validatorMiddleware({ eventSchema, responseSchema }))
.use(httpErrorHandlerMiddleware())
.handler((event, context, { signal }) => {
// ...
})
```
1 change: 1 addition & 0 deletions website/docs/middlewares/event-normalizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html
| SES | No | Normalization not required |
| SNS | Yes | JSON parse `Message` |
| SQS | Yes | JSON parse `body` |
| VPC Lattice | No \* | See middleware prefixed with `@middy/http-` |

\* Handled in another dedicated middleware(s)

Expand Down
6 changes: 1 addition & 5 deletions website/docs/middlewares/http-event-normalizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ to fail with the error: `TypeError: Cannot read property 'userId' of undefined`.
A simple solution would be to add an `if` statement to verify if the `pathParameters` (or `queryStringParameters`/`multiValueQueryStringParameters`)
exists before accessing one of its parameters, but this approach is very verbose and error prone.

This middleware normalizes the API Gateway event, making sure that an object for
`queryStringParameters`, `multiValueQueryStringParameters` and `pathParameters` is always available (resulting in empty objects
when no parameter is available), this way you don't have to worry about adding extra `if`
statements before trying to read a property and calling `event.pathParameters.userId` will
result in `undefined` when no path parameter is available, but not in an error.
This middleware normalizes the API Gateway, ALB, Function URLs, and VPC Lattice events, making sure that an object for `queryStringParameters`, `multiValueQueryStringParameters`, `pathParameters`, and `isBase64Encoded` is always available (resulting in empty objects when no parameter is available), this way you don't have to worry about adding extra `if` statements before trying to read a property and calling `event.pathParameters.userId` will result in `undefined` when no path parameter is available, but not in an error.

> Important note : API Gateway HTTP API format 2.0 doesn't have `multiValueQueryStringParameters` fields. Duplicate query strings are combined with commas and included in the `queryStringParameters` field.
Expand Down

0 comments on commit 06694fc

Please sign in to comment.