UNPKG

@middy/core

Version:

🛵 The stylish Node.js middleware engine for AWS Lambda (core package)

245 lines (219 loc) • 7.07 kB
/* global awslambda */ import { Readable } from 'node:stream' import { ReadableStream } from 'node:stream/web' import { pipeline } from 'node:stream/promises' import { setTimeout } from 'node:timers' const defaultLambdaHandler = () => {} const defaultPlugin = { timeoutEarlyInMillis: 5, timeoutEarlyResponse: () => { const err = new Error('[AbortError]: The operation was aborted.', { cause: { package: '@middy/core' } }) err.name = 'TimeoutError' throw err }, streamifyResponse: false // Deprecate need for this when AWS provides a flag for when it's looking for it } const middy = (lambdaHandler = defaultLambdaHandler, plugin = {}) => { // Allow base handler to be set using .handler() if (typeof lambdaHandler !== 'function') { plugin = lambdaHandler lambdaHandler = defaultLambdaHandler } plugin = { ...defaultPlugin, ...plugin } plugin.timeoutEarly = plugin.timeoutEarlyInMillis > 0 plugin.beforePrefetch?.() const beforeMiddlewares = [] const afterMiddlewares = [] const onErrorMiddlewares = [] const middyHandler = (event = {}, context = {}) => { plugin.requestStart?.() const request = { event, context, response: undefined, error: undefined, internal: plugin.internal ?? {} } return runRequest( request, beforeMiddlewares, lambdaHandler, afterMiddlewares, onErrorMiddlewares, plugin ) } const middy = plugin.streamifyResponse ? awslambda.streamifyResponse(async (event, responseStream, context) => { const handlerResponse = await middyHandler(event, context) let handlerBody = handlerResponse if (handlerResponse.statusCode) { handlerBody = handlerResponse.body ?? '' delete handlerResponse.body // #1137 responseStream = awslambda.HttpResponseStream.from( responseStream, handlerResponse ) } let handlerStream if (handlerBody._readableState || handlerBody instanceof ReadableStream) { handlerStream = handlerBody } else if (typeof handlerBody === 'string') { // #1189 handlerStream = Readable.from( handlerBody.length < stringIteratorSize ? handlerBody : stringIterator(handlerBody) ) } if (!handlerStream) { throw new Error('handler response not a ReadableStream') } await pipeline(handlerStream, responseStream) }) : middyHandler middy.use = (middlewares) => { if (!Array.isArray(middlewares)) { middlewares = [middlewares] } for (const middleware of middlewares) { const { before, after, onError } = middleware if (before || after || onError) { if (before) middy.before(before) if (after) middy.after(after) if (onError) middy.onError(onError) } else { throw new Error( 'Middleware must be an object containing at least one key among "before", "after", "onError"' ) } } return middy } // Inline Middlewares middy.before = (beforeMiddleware) => { beforeMiddlewares.push(beforeMiddleware) return middy } middy.after = (afterMiddleware) => { afterMiddlewares.unshift(afterMiddleware) return middy } middy.onError = (onErrorMiddleware) => { onErrorMiddlewares.unshift(onErrorMiddleware) return middy } middy.handler = (replaceLambdaHandler) => { lambdaHandler = replaceLambdaHandler return middy } return middy } const stringIteratorSize = 16384 // 16 * 1024 // Node.js default function * stringIterator (input) { let position = 0 const length = input.length while (position < length) { yield input.substring(position, position + stringIteratorSize) position += stringIteratorSize } } // shared AbortController, because it's slow let handlerAbort = new AbortController() const runRequest = async ( request, beforeMiddlewares, lambdaHandler, afterMiddlewares, onErrorMiddlewares, plugin ) => { let timeoutID // context.getRemainingTimeInMillis checked for when AWS context missing (tests, containers) const timeoutEarly = plugin.timeoutEarly && request.context.getRemainingTimeInMillis try { await runMiddlewares(request, beforeMiddlewares, plugin) // Check if before stack hasn't exit early if (!Object.prototype.hasOwnProperty.call(request, 'earlyResponse')) { plugin.beforeHandler?.() // Can't manually abort and timeout with same AbortSignal // https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static if (handlerAbort.signal.aborted) { handlerAbort = new AbortController() } const promises = [ lambdaHandler(request.event, request.context, { signal: handlerAbort.signal }) ] // clearTimeout pattern is 10x faster than using AbortController // Note: signal.abort is slow ~6000ns if (timeoutEarly) { let timeoutResolve const timeoutPromise = new Promise((resolve, reject) => { timeoutResolve = () => { handlerAbort.abort() try { resolve(plugin.timeoutEarlyResponse()) } catch (e) { reject(e) } } }) timeoutID = setTimeout( timeoutResolve, request.context.getRemainingTimeInMillis() - plugin.timeoutEarlyInMillis ) promises.push(timeoutPromise) } request.response = await Promise.race(promises) if (timeoutID) { clearTimeout(timeoutID) } plugin.afterHandler?.() await runMiddlewares(request, afterMiddlewares, plugin) } } catch (e) { // timeout should be aborted when errors happen in handler if (timeoutID) { clearTimeout(timeoutID) } // Reset response changes made by after stack before error thrown request.response = undefined request.error = e try { await runMiddlewares(request, onErrorMiddlewares, plugin) } catch (e) { // Save error that wasn't handled e.originalError = request.error request.error = e throw request.error } // Catch if onError stack hasn't handled the error if (typeof request.response === 'undefined') throw request.error } finally { await plugin.requestEnd?.(request) } return request.response } const runMiddlewares = async (request, middlewares, plugin) => { for (const nextMiddleware of middlewares) { plugin.beforeMiddleware?.(nextMiddleware.name) const res = await nextMiddleware(request) plugin.afterMiddleware?.(nextMiddleware.name) // short circuit chaining and respond early if (typeof res !== 'undefined') { request.earlyResponse = res } // earlyResponse pattern added in 6.0.0 to handle undefined values if (Object.prototype.hasOwnProperty.call(request, 'earlyResponse')) { request.response = request.earlyResponse return } } } export default middy