UNPKG

hastily

Version:

express middleware to simulate fastly cdn

157 lines 13.6 kB
/** * Simulate the [Fastly ImageOpto](https://docs.fastly.com/api/imageopto/) API. * Returns a middleware which attempts to run image responses through `sharp`. */ import { createLogger } from './logging'; import mime from 'mime-types'; import sharp from 'sharp'; import { URLSearchParams } from 'url'; import vary from 'vary'; import RequestErrors from './errors'; import FastlyParams from './fastly-params'; import mapOptions from './map-options'; import splice from './splice-response'; export const HASTILY_HEADER = { NAME: 'X-Optimized', VALUE: 'hastily', }; export const HASTILY_STREAMABLE_FILETYPES = new Set(Object.keys(sharp.format).filter((ext) => sharp.format[ext].input.stream)); // plus the one sharp doesn't mention outright, the alias to jpeg... HASTILY_STREAMABLE_FILETYPES.add('jpg'); // minus SVGs, which are widely supported in 2019 and we should not rasterize HASTILY_STREAMABLE_FILETYPES.delete('svg'); export const HASTILY_STREAMABLE_PATH_REGEXP = new RegExp(`/.+\\.(${[...HASTILY_STREAMABLE_FILETYPES].join('|')})(?:[?#].*)?`); /** * Use the `sharp.format` manifest to determine if the current request's file * extension matches a format that sharp can stream in to optimize. * @param req {Request} */ export const hasSupportedExtension = (req) => HASTILY_STREAMABLE_PATH_REGEXP.test(req.originalUrl); /** * Returns a new imageopto middleware for use in Express `app.use()`. * Won't do anything if the Express app isn't already serving images! * */ export function imageopto(filterOrOpts) { const options = { filter: hasSupportedExtension, force: false, }; if (typeof filterOrOpts === 'function') { options.filter = filterOrOpts; } else if (typeof filterOrOpts === 'object') { options.filter = filterOrOpts.filter || options.filter; options.force = filterOrOpts.force; options.errorLog = options.quiet ? (_) => void 0 : filterOrOpts.errorLog; } const constructorLog = createLogger('middleware'); constructorLog.debug('creating new middleware'); if (options.filter === hasSupportedExtension) { constructorLog.debug('middleware filtering req.path for %s', HASTILY_STREAMABLE_PATH_REGEXP.source); } const requestLog = createLogger('request'); return function hastily(req, res, next) { const reqLog = requestLog.child({ req }); if (!options.filter(req)) { reqLog.debug('did not pass supplied filter function'); return next(); } splice(req, res, next, () => { // determine if the entity should be transformed if (!shouldTransform(req, res, reqLog, options)) { return false; } reqLog.debug('hastily will handle this image by transforming the response through sharp'); vary(res, 'Accept'); res.setHeader(HASTILY_HEADER.NAME, HASTILY_HEADER.VALUE); // image opto stream const params = new FastlyParams(new Map(new URLSearchParams(req.query).entries()), req, res); const sharpStream = sharp(); const emitSharpError = (error) => { if (options.errorLog) { options.errorLog(error); } else { reqLog.error('Image processing failed: %s', error.toString()); } }; sharpStream.on('error', emitSharpError); try { const transformStream = mapOptions(params, sharpStream); const warnings = params.getWarnings(); if (warnings.length > 0) { const requestErrors = new RequestErrors(req.url, warnings); if (options.errorLog) { options.errorLog(requestErrors); } else { reqLog.warn(requestErrors.toString()); } } reqLog.debug('mapped options and created sharp stream'); return transformStream; } catch (e) { emitSharpError(e); return false; } }); }; } const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/; /** * Determine if the entity should be transformed. * @private */ function shouldTransform(req, res, reqLog, options) { if (req.method !== 'GET') { reqLog.debug('no transform: request method must be GET but is %s', req.method); return false; } if (res.statusCode > 299 || res.statusCode < 200) { reqLog.debug('no transform: res.statusCode must be 2xx but is %s', res.statusCode); return false; } const cacheControl = res.getHeader('Cache-Control'); // Don't optimize for Cache-Control: no-transform // https://tools.ietf.org/html/rfc7234#section-5.2.2.4 if (cacheControl && cacheControlNoTransformRegExp.test(cacheControl.toString())) { reqLog.debug('no transform: cache control header: "%s"', cacheControl); return false; } if (!options.force) { // Don't optimize if we've already done it somewhere const hastilyHeader = res.getHeader(HASTILY_HEADER.NAME); if (hastilyHeader === HASTILY_HEADER.VALUE) { reqLog.warn('no transform: header %o, hastily alrady transformed this earlier', HASTILY_HEADER); return false; } // Don't optimize if Fastly has already done it for us const fastlyHeader = res.getHeader('fastly-io-info'); if (fastlyHeader) { reqLog.debug('no transform: fastly already transformed according to fastly-io-info header: "%s"', fastlyHeader); return false; } } const contentType = res.getHeader('content-type'); const extension = mime.extension(contentType); if (!extension) { reqLog.error('no transform: no valid content-type could not be detected'); return false; } const sharpFormatCapabilities = sharp.format[extension]; if (!sharpFormatCapabilities || !sharpFormatCapabilities.input.stream) { reqLog.error('no transform: sharp does not support input of type "%s"', contentType); return false; } const contentEncoding = res.getHeader('content-encoding'); if (contentEncoding && typeof contentEncoding === 'string' && contentEncoding.trim()) { reqLog.error('no transform: image is compressed with content-encoding "%s"; hastily does not support decompressing images. The server *should not* be compressing images! HTTP content encoding is only recommended for text documents. Binary files will not get smaller, and may evem get larger!'); } return true; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1hZ2VvcHRvLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2xpYi9pbWFnZW9wdG8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUV6QyxPQUFPLElBQUksTUFBTSxZQUFZLENBQUM7QUFDOUIsT0FBTyxLQUFxQixNQUFNLE9BQU8sQ0FBQztBQUMxQyxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBQ3RDLE9BQU8sSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUN4QixPQUFPLGFBQWEsTUFBTSxVQUFVLENBQUM7QUFDckMsT0FBTyxZQUFZLE1BQU0saUJBQWlCLENBQUM7QUFTM0MsT0FBTyxVQUFVLE1BQU0sZUFBZSxDQUFDO0FBQ3ZDLE9BQU8sTUFBTSxNQUFNLG1CQUFtQixDQUFDO0FBb0J2QyxNQUFNLENBQUMsTUFBTSxjQUFjLEdBQUc7SUFDNUIsSUFBSSxFQUFFLGFBQWE7SUFDbkIsS0FBSyxFQUFFLFNBQVM7Q0FDakIsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLDRCQUE0QixHQUFHLElBQUksR0FBRyxDQUNqRCxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxNQUFNLENBQzlCLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLEdBQXVCLENBQUMsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUM1RCxDQUNGLENBQUM7QUFDRixvRUFBb0U7QUFDcEUsNEJBQTRCLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3hDLDZFQUE2RTtBQUM3RSw0QkFBNEIsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDM0MsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUcsSUFBSSxNQUFNLENBQ3RELFVBQVUsQ0FBQyxHQUFHLDRCQUE0QixDQUFD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