@mockyeah/server
Version:
A powerful service mocking, recording, and playback utility.
155 lines (125 loc) • 3.9 kB
JavaScript
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;