@architect/functions
Version:
Runtime utility library for Functional Web Apps (FWAs) built with Architect (https://arc.codes)
208 lines (186 loc) • 7.11 kB
JavaScript
let httpError = require('./errors')
let binaryTypes = require('./helpers/binary-types')
let { brotliCompressSync: br, gzipSync: gzip } = require('zlib')
let compressionTypes = { br, gzip }
module.exports = function responseFormatter (req, params) {
let isError = params instanceof Error
// Handle HTTP API v2.0 payload scenarios, which have some very strange edges
if (req?.version === '2.0') {
// New school AWS
let knownParams = [ 'statusCode', 'body', 'headers', 'isBase64Encoded', 'cookies' ]
let hasKnownParams = p => knownParams.some(k => k === p)
// Old school Arc
let tidyParams = [ 'code', 'cookie', 'cors', 'location', 'session', 'status' ]
let hasTidyParams = p => tidyParams.some(k => k === p)
// Older school Arc
let staticallyBound = [ 'html', 'css', 'js', 'text', 'json', 'xml' ]
let isStaticallyBound = p => staticallyBound.some(k => k === p)
let is = t => typeof params === t
let keys = (params && is('object') && Object.keys(params)) || []
// Handle scenarios where we have a known parameter returned
if (is('object') &&
!Array.isArray(params) &&
(keys.some(hasKnownParams) ||
keys.some(hasTidyParams) ||
keys.some(isStaticallyBound))) {
/* noop */
}
else if (isError) {
/* noop */
}
// Handle scenarios where arbitrary stuff is returned to be JSONified
else if (is('object') ||
is('number') ||
(is('string') && params.length) ||
Array.isArray(params) ||
params instanceof Buffer) {
params = { body: JSON.stringify(params) }
}
// Not returning is actually valid now lolnothingmatters
else if (!params) {
params = {}
}
}
let buffer
let bodyIsBuffer = params?.body instanceof Buffer
if (bodyIsBuffer) buffer = params.body // Back up buffer
if (!isError) params = JSON.parse(JSON.stringify(params)) // Deep copy to aid testing mutation
if (bodyIsBuffer) params.body = buffer // Restore non-JSON-encoded buffer
/**
* Response defaults
* where possible, normalize headers to pascal-kebab case (lolsigh)
*/
// Body
let body = params.body || ''
// Headers: cache-control
let cacheControl = params.cacheControl ||
params.headers?.['cache-control'] ||
params.headers?.['Cache-Control'] || ''
if (params.headers?.['Cache-Control']) {
delete params.headers['Cache-Control'] // Clean up improper casing
}
// Headers: content-type
let type = params.type ||
params.headers?.['content-type'] ||
params.headers?.['Content-Type'] ||
'application/json; charset=utf8'
if (params.headers?.['Content-Type']) {
delete params.headers['Content-Type'] // Clean up improper casing
}
// Headers: content-encoding
let encoding = params.headers?.['content-encoding'] ||
params.headers?.['Content-Encoding']
if (params.headers?.['Content-Encoding']) {
delete params.headers['Content-Encoding'] // Clean up improper casing
}
let acceptEncoding = (req.headers?.['accept-encoding'] ||
req.headers?.['Accept-Encoding'])
// Cross-origin ritual sacrifice
let cors = params.cors
// Old school convenience response params
// As of Functions v4 we will keep these around for all eternity
if (params.html) {
type = 'text/html; charset=utf8'
body = params.html
}
else if (params.css) {
type = 'text/css; charset=utf8'
body = params.css
}
else if (params.js) {
type = 'text/javascript; charset=utf8'
body = params.js
}
else if (params.text) {
type = 'text/plain; charset=utf8'
body = params.text
}
else if (params.json) {
type = 'application/json; charset=utf8'
body = JSON.stringify(params.json)
}
else if (params.xml) {
type = 'application/xml; charset=utf8'
body = params.xml
}
// Status
let providedStatus = params.status || params.code || params.statusCode
let statusCode = providedStatus || 200
let res = {
headers: Object.assign({}, { 'content-type': type }, params.headers || {}),
statusCode,
body,
}
// REST API stuff
if (params.multiValueHeaders) {
res.multiValueHeaders = params.multiValueHeaders
}
// HTTP API stuff
if (params.cookies) {
res.cookies = params.cookies
}
// Error override
if (isError) {
let statusCode = providedStatus || 500
let title = params.name
let message = `
${params.message}<br>
<pre>${params.stack}<pre>
`
res = httpError({ statusCode, title, message })
}
// Set and/or update headers
let headers = res.headers
if (cacheControl) headers['cache-control'] = cacheControl
let antiCache = type.includes('text/html') ||
type.includes('application/json') ||
type.includes('application/vnd.api+json') ||
params.location // Ensure CDNs don't cache location responses
if (headers && !headers['cache-control'] && antiCache) {
headers['cache-control'] = 'no-cache, no-store, must-revalidate, max-age=0, s-maxage=0'
}
else if (headers && !headers['cache-control']) {
headers['cache-control'] = 'max-age=86400' // Default cache to one day unless otherwise specified
}
if (cors) headers['access-control-allow-origin'] = '*'
if (params.isBase64Encoded) res.isBase64Encoded = true
if (params.location) {
res.statusCode = providedStatus || 302
res.headers.location = params.location
}
// Handle body encoding (if necessary)
let [ contentType ] = (res.headers['content-type'] || '').split(';')
let isBinary = binaryTypes.includes(contentType)
let bodyIsString = typeof res.body === 'string'
let b64enc = i => new Buffer.from(i).toString('base64')
function compress (body, type) {
res.headers['content-encoding'] = type
return compressionTypes[type](body)
}
// Compress, encode, and flag buffer responses
// Legacy API Gateway (REST, i.e. !req.version) and ASAP (which sets isBase64Encoded) handle their own compression, so don't double-compress / encode
let shouldCompress = req.version && !params.isBase64Encoded && !encoding && acceptEncoding && params.compression !== false
if (bodyIsBuffer) {
let body = shouldCompress ? compress(res.body) : res.body
res.body = b64enc(body)
res.isBase64Encoded = true
}
// Body is likely already base64 encoded & has binary MIME type, so just flag it
else if (bodyIsString && isBinary) {
res.isBase64Encoded = true
}
// Compress, encode, and flag string responses
else if (bodyIsString && shouldCompress) {
let accepted = acceptEncoding.split(', ')
let compression
/**/ if (accepted.includes('br')) compression = 'br'
else if (accepted.includes('gzip')) compression = 'gzip'
else return res
if (compressionTypes[params.compression] && accepted.includes(params.compression)) {
compression = params.compression
}
res.body = b64enc(compress(res.body, compression))
res.isBase64Encoded = true
}
return res
}