hastily
Version:
express middleware to simulate fastly cdn
164 lines • 14.1 kB
JavaScript
;
/**
* Simulate the [Fastly ImageOpto](https://docs.fastly.com/api/imageopto/) API.
* Returns a middleware which attempts to run image responses through `sharp`.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.imageopto = exports.hasSupportedExtension = exports.HASTILY_STREAMABLE_PATH_REGEXP = exports.HASTILY_STREAMABLE_FILETYPES = exports.HASTILY_HEADER = void 0;
const logging_1 = require("./logging");
const mime_types_1 = __importDefault(require("mime-types"));
const sharp_1 = __importDefault(require("sharp"));
const url_1 = require("url");
const vary_1 = __importDefault(require("vary"));
const errors_1 = __importDefault(require("./errors"));
const fastly_params_1 = __importDefault(require("./fastly-params"));
const map_options_1 = __importDefault(require("./map-options"));
const splice_response_1 = __importDefault(require("./splice-response"));
exports.HASTILY_HEADER = {
NAME: 'X-Optimized',
VALUE: 'hastily',
};
exports.HASTILY_STREAMABLE_FILETYPES = new Set(Object.keys(sharp_1.default.format).filter((ext) => sharp_1.default.format[ext].input.stream));
// plus the one sharp doesn't mention outright, the alias to jpeg...
exports.HASTILY_STREAMABLE_FILETYPES.add('jpg');
// minus SVGs, which are widely supported in 2019 and we should not rasterize
exports.HASTILY_STREAMABLE_FILETYPES.delete('svg');
exports.HASTILY_STREAMABLE_PATH_REGEXP = new RegExp(`/.+\\.(${[...exports.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}
*/
exports.hasSupportedExtension = (req) => exports.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!
*
*/
function imageopto(filterOrOpts) {
const options = {
filter: exports.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 = logging_1.createLogger('middleware');
constructorLog.debug('creating new middleware');
if (options.filter === exports.hasSupportedExtension) {
constructorLog.debug('middleware filtering req.path for %s', exports.HASTILY_STREAMABLE_PATH_REGEXP.source);
}
const requestLog = logging_1.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_response_1.default(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_1.default(res, 'Accept');
res.setHeader(exports.HASTILY_HEADER.NAME, exports.HASTILY_HEADER.VALUE);
// image opto stream
const params = new fastly_params_1.default(new Map(new url_1.URLSearchParams(req.query).entries()), req, res);
const sharpStream = sharp_1.default();
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 = map_options_1.default(params, sharpStream);
const warnings = params.getWarnings();
if (warnings.length > 0) {
const requestErrors = new errors_1.default(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;
}
});
};
}
exports.imageopto = imageopto;
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(exports.HASTILY_HEADER.NAME);
if (hastilyHeader === exports.HASTILY_HEADER.VALUE) {
reqLog.warn('no transform: header %o, hastily alrady transformed this earlier', exports.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_types_1.default.extension(contentType);
if (!extension) {
reqLog.error('no transform: no valid content-type could not be detected');
return false;
}
const sharpFormatCapabilities = sharp_1.default.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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1hZ2VvcHRvLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2xpYi9pbWFnZW9wdG8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7R0FHRzs7Ozs7O0FBRUgsdUNBQXlDO0FBRXpDLDREQUE4QjtBQUM5QixrREFBMEM7QUFDMUMsNkJBQXNDO0FBQ3RDLGdEQUF3QjtBQUN4QixzREFBcUM7QUFDckMsb0VBQTJDO0FBUzNDLGdFQUF1QztBQUN2Qyx3RUFBdUM7QUFvQjFCLFFBQUEsY0FBYyxHQUFHO0lBQzVCLElBQUksRUFBRSxhQUFhO0lBQ25CLEtBQUssRUFBRSxTQUFTO0NBQ2pCLENBQUM7QUFFVyxRQUFBLDRCQUE0QixHQUFHLElBQUksR0FBRyxDQUNqRCxNQUFNLENBQUMsSUFBSSxDQUFDLGVBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxNQUFNLENBQzlCLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxlQUFLLENBQUMsTUFBTSxDQUFDLEdBQXVCLENBQUMsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUM1RCxDQUNGLENBQUM7QUFDRixvRUFBb0U7QUFDcEUsb0NBQTRCLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3hDLDZFQUE2RTtBQUM3RSxvQ0FBNEIsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDOUIsUUFBQSw4QkFBOEIsR0FBRyxJQUFJLE1BQU0sQ0FDdEQsVUFBVSxDQUFDLEdBQUcsb0NBQTRCLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FDcEUsQ0FBQztBQUVGOzs7O0dBSUc7QUFDVSxRQUFBLHFCQUFxQixHQUFrQixDQUFDLEdBQUcsRUFBRSxFQUFFLENBQzFELHNDQUE4QixDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsV0FBVyxDQUFDLENBQUM7QUFFdkQ7Ozs7R0FJRztBQUNILFNBQWdCLFNBQVMsQ0FDdkIsWUFBOEM7SUFFOUMsTUFBTSxPQUFPLEdB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