hastily
Version:
express middleware to simulate fastly cdn
157 lines • 13.6 kB
JavaScript
/**
* 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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