UNPKG

@mockyeah/server

Version:

A powerful service mocking, recording, and playback utility.

155 lines (125 loc) 3.9 kB
const { isPlainObject, flatten, omitBy } = require('lodash'); const { decodeProtocolAndPort } = require('./helpers'); const proxyRecord = require('./proxyRecord'); const getDynamicSuites = require('./getDynamicSuites'); const openingSlashRegex = /^\//; const leadUrlEncodedProtocolRegex = /^(https?)%3A%2F%2F/; const makeRequestUrl = req => { const isAbsolute = /^\/*https?[:~][/~]{2}/.test(decodeURIComponent(req.originalUrl)); return isAbsolute ? decodeProtocolAndPort( req.originalUrl .replace(openingSlashRegex, '') .replace(leadUrlEncodedProtocolRegex, (match, p1) => `${p1}://`) ) : req.originalUrl; }; const makeRequestOptions = req => { const { headers: _headers, method: _method, rawBody } = req; const headers = Object.assign({}, _headers); const method = _method.toLowerCase(); // Recording `host` header is bad for proxy behavior. delete headers.host; // TODO: Should we support an option to rewrite `origin` header? const reqUrl = makeRequestUrl(req); return { method, url: reqUrl, // TODO: Should we even record headers? Optional? headers, body: rawBody }; }; const proxyRoute = (req, res, next) => { const { app } = req; if (app.config.journal) { app.log( ['request', 'journal'], JSON.stringify( { url: req.url, fullUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`, clientIp: req.headers['x-forwarded-for'] || req.connection.remoteAddress, method: req.method, headers: req.headers, query: req.query, body: req.body, cookies: req.cookies }, null, 2 ) ); } const reqUrl = makeRequestUrl(req); const startTime = new Date().getTime(); const requestOptions = makeRequestOptions(req); const fetchOptions = { method: (requestOptions.method || 'GET').toUpperCase(), headers: requestOptions.headers, body: requestOptions.body }; const dynamicMocks = flatten(getDynamicSuites(app, req)); app.locals.mockyeahFetch .fetch(reqUrl, fetchOptions, { noProxy: !app.locals.proxying, dynamicMocks }) .then(fetchRes => { if (!fetchRes.body) { return { fetchRes }; } return fetchRes.text().then(body => ({ fetchRes, body })); }) .then(({ fetchRes, body }) => { // TODO: Handle all forms of headers including object, tuple array, Headers instance, etc. let headers = {}; if (fetchRes.headers) { if (typeof fetchRes.headers.forEach === 'function') { fetchRes.headers.forEach((v, k) => { headers[k] = v; }); } else if (isPlainObject(fetchRes.headers)) { headers = Object.entries(fetchRes.headers).forEach(([k, v]) => { headers[k] = v; }); } } delete headers['content-encoding']; if (headers['x-mockyeah-proxied'] === 'true') { app.log(['proxy', req.method], reqUrl); } if (!app.config.responseHeaders) { headers = omitBy(headers, (v, k) => k.startsWith('x-mockyeah')); } Object.entries(headers).forEach(([k, v]) => { res.set(k, v); }); const { status } = fetchRes; if (app.locals.recording) { proxyRecord({ app, req, reqUrl, startTime, body, headers, status }); } res.status(status); // TODO: Support streaming response back, even when also buffering for recording. if (!body) { res.end(); } else { res.send(body); } }) .catch(err => { app.log(['proxy', 'error'], err.message); next(err); }); }; // Export for testing purposes. proxyRoute.makeRequestUrl = makeRequestUrl; module.exports = proxyRoute;