Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@sitecore-jss/sitecore-jss-proxy

Package Overview
Dependencies
Maintainers
0
Versions
1399
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sitecore-jss/sitecore-jss-proxy - npm Package Compare versions

Comparing version 22.3.0-canary.15 to 22.3.0

dist/cjs/AppRenderer.js

466

dist/cjs/index.js
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rewriteRequestPath = exports.removeEmptyAnalyticsCookie = void 0;
const http_proxy_middleware_1 = require("http-proxy-middleware");
const http_status_codes_1 = __importDefault(require("http-status-codes"));
const set_cookie_parser_1 = __importDefault(require("set-cookie-parser"));
const zlib_1 = __importDefault(require("zlib")); // node.js standard lib
const util_1 = require("./util");
// For some reason, every other response returned by Sitecore contains the 'set-cookie' header with the SC_ANALYTICS_GLOBAL_COOKIE value as an empty string.
// This effectively sets the cookie to empty on the client as well, so if a user were to close their browser
// after one of these 'empty value' responses, they would not be tracked as a returning visitor after re-opening their browser.
// To address this, we simply parse the response cookies and if the analytics cookie is present but has an empty value, then we
// remove it from the response header. This means the existing cookie in the browser remains intact.
const removeEmptyAnalyticsCookie = (proxyResponse) => {
const cookies = set_cookie_parser_1.default.parse(proxyResponse.headers['set-cookie']);
if (cookies) {
const analyticsCookieIndex = cookies.findIndex((c) => c.name === 'SC_ANALYTICS_GLOBAL_COOKIE');
if (analyticsCookieIndex !== -1) {
const analyticsCookie = cookies[analyticsCookieIndex];
if (analyticsCookie && analyticsCookie.value === '') {
cookies.splice(analyticsCookieIndex, 1);
/* eslint-disable no-param-reassign */
proxyResponse.headers['set-cookie'] = cookies;
/* eslint-enable no-param-reassign */
}
}
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GRAPHQL_LAYOUT_QUERY_NAME = void 0;
__exportStar(require("./middleware"), exports);
__exportStar(require("./types"), exports);
__exportStar(require("./personalize"), exports);
var layout_1 = require("@sitecore-jss/sitecore-jss/layout");
Object.defineProperty(exports, "GRAPHQL_LAYOUT_QUERY_NAME", { enumerable: true, get: function () { return layout_1.GRAPHQL_LAYOUT_QUERY_NAME; } });
exports.removeEmptyAnalyticsCookie = removeEmptyAnalyticsCookie;
// inspired by: http://stackoverflow.com/a/22487927/9324
/**
* @param {IncomingMessage} proxyResponse
* @param {IncomingMessage} request
* @param {ServerResponse} serverResponse
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
*/
function renderAppToResponse(proxyResponse, request, serverResponse, renderer, config) {
return __awaiter(this, void 0, void 0, function* () {
// monkey-patch FTW?
const originalWriteHead = serverResponse.writeHead;
const originalWrite = serverResponse.write;
const originalEnd = serverResponse.end;
// these lines are necessary and must happen before we do any writing to the response
// otherwise the headers will have already been sent
delete proxyResponse.headers['content-length'];
proxyResponse.headers['content-type'] = 'text/html; charset=utf-8';
// remove IIS server header for security
delete proxyResponse.headers.server;
if (config.setHeaders) {
config.setHeaders(request, serverResponse, proxyResponse);
}
const contentEncoding = proxyResponse.headers['content-encoding'];
if (contentEncoding &&
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) {
delete proxyResponse.headers['content-encoding'];
}
// we are going to set our own status code if rendering fails
serverResponse.writeHead = () => serverResponse;
// buffer the response body as it is written for later processing
let buf = Buffer.from('');
serverResponse.write = (data, encoding) => {
if (Buffer.isBuffer(data)) {
buf = Buffer.concat([buf, data]); // append raw buffer
}
else {
buf = Buffer.concat([buf, Buffer.from(data, encoding)]); // append string with optional character encoding (default utf8)
}
// sanity check: if the response is huge, bail.
// ...we don't want to let someone bring down the server by filling up all our RAM.
if (buf.length > config.maxResponseSizeBytes) {
throw new Error('Document too large');
}
return true;
};
/**
* Extract layout service data from proxy response
*/
function extractLayoutServiceDataFromProxyResponse() {
return __awaiter(this, void 0, void 0, function* () {
if (proxyResponse.statusCode === http_status_codes_1.default.OK ||
proxyResponse.statusCode === http_status_codes_1.default.NOT_FOUND) {
let responseString;
if (contentEncoding &&
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) {
responseString = new Promise((resolve, reject) => {
if (config.debug) {
console.log('Layout service response is compressed; decompressing.');
}
zlib_1.default.unzip(buf, (error, result) => {
if (error) {
reject(error);
}
if (result) {
resolve(result.toString('utf-8'));
}
});
});
}
else {
responseString = Promise.resolve(buf.toString('utf-8'));
}
return responseString.then(util_1.tryParseJson);
}
return Promise.resolve(null);
});
}
/**
* function replies with HTTP 500 when an error occurs
* @param {Error} error
*/
function replyWithError(error) {
return __awaiter(this, void 0, void 0, function* () {
console.error(error);
let errorResponse = {
statusCode: proxyResponse.statusCode || http_status_codes_1.default.INTERNAL_SERVER_ERROR,
content: proxyResponse.statusMessage || 'Internal Server Error',
headers: {},
};
if (config.onError) {
const onError = yield config.onError(error, proxyResponse);
errorResponse = Object.assign(Object.assign({}, errorResponse), onError);
}
completeProxyResponse(Buffer.from(errorResponse.content), errorResponse.statusCode, errorResponse.headers);
});
}
// callback handles the result of server-side rendering
/**
* @param {Error | null} error
* @param {RenderResponse} result
*/
function handleRenderingResult(error, result) {
return __awaiter(this, void 0, void 0, function* () {
if (!error && !result) {
return replyWithError(new Error('Render function did not return a result or an error!'));
}
if (error) {
return replyWithError(error);
}
if (!result) {
// should not occur, but makes TS happy
return replyWithError(new Error('Render function result did not return a result.'));
}
if (!result.html) {
return replyWithError(new Error('Render function result was returned but html property was falsy.'));
}
if (config.transformSSRContent) {
result.html = yield config.transformSSRContent(result, request, serverResponse);
}
// we have to convert back to a buffer so that we can get the *byte count* (rather than character count) of the body
let content = Buffer.from(result.html);
// setting the content-length header is not absolutely necessary, but is recommended
proxyResponse.headers['content-length'] = content.length.toString(10);
// if original request was a HEAD, we should not return a response body
if (request.method === 'HEAD') {
if (config.debug) {
console.log('DEBUG: Original request method was HEAD, clearing response body');
}
content = Buffer.from([]);
}
if (result.redirect) {
if (!result.status) {
result.status = 302;
}
proxyResponse.headers.location = result.redirect;
}
const finalStatusCode = result.status || proxyResponse.statusCode || http_status_codes_1.default.OK;
if (config.debug) {
console.log('DEBUG: FINAL response headers for client', JSON.stringify(proxyResponse.headers, null, 2));
console.log('DEBUG: FINAL status code for client', finalStatusCode);
}
completeProxyResponse(content, finalStatusCode);
});
}
/**
* @param {Buffer | null} content
* @param {number} statusCode
* @param {IncomingHttpHeaders} [headers]
*/
function completeProxyResponse(content, statusCode, headers) {
if (!headers) {
headers = proxyResponse.headers;
}
originalWriteHead.apply(serverResponse, [statusCode, headers]);
if (content)
originalWrite.call(serverResponse, content);
originalEnd.call(serverResponse);
}
/**
* @param {object} layoutServiceData
*/
function createViewBag(layoutServiceData) {
return __awaiter(this, void 0, void 0, function* () {
let viewBag = {
statusCode: proxyResponse.statusCode,
dictionary: {},
};
if (config.createViewBag) {
const customViewBag = yield config.createViewBag(request, serverResponse, proxyResponse, layoutServiceData);
viewBag = Object.assign(Object.assign({}, viewBag), customViewBag);
}
return viewBag;
});
}
// as the response is ending, we parse the current response body which is JSON, then
// render the app using that JSON, but return HTML to the final response.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
serverResponse.end = () => __awaiter(this, void 0, void 0, function* () {
try {
const layoutServiceData = yield extractLayoutServiceDataFromProxyResponse();
const viewBag = yield createViewBag(layoutServiceData);
if (!layoutServiceData) {
throw new Error(`Received invalid response ${proxyResponse.statusCode} ${proxyResponse.statusMessage}`);
}
return renderer(handleRenderingResult,
// originalUrl not defined in `http-proxy-middleware` types but it exists
request.originalUrl, layoutServiceData, viewBag);
}
catch (error) {
return replyWithError(error);
}
});
});
}
/**
* @param {IncomingMessage} proxyResponse
* @param {Request} request
* @param {Response} serverResponse
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
*/
function handleProxyResponse(proxyResponse, request, serverResponse, renderer, config) {
(0, exports.removeEmptyAnalyticsCookie)(proxyResponse);
if (config.debug) {
console.log('DEBUG: request url', request.url);
console.log('DEBUG: request query', request.query);
console.log('DEBUG: request original url', request.originalUrl);
console.log('DEBUG: proxied request response code', proxyResponse.statusCode);
console.log('DEBUG: RAW request headers', JSON.stringify(request.headers, null, 2));
console.log('DEBUG: RAW headers from the proxied response', JSON.stringify(proxyResponse.headers, null, 2));
}
// if the request URL contains any of the excluded rewrite routes, we assume the response does not need to be server rendered.
// instead, the response should just be relayed as usual.
if (isUrlIgnored(request.originalUrl, config, true)) {
return Promise.resolve(undefined);
}
// your first thought might be: why do we need to render the app here? why not just pass the JSON response to another piece of middleware that will render the app?
// the answer: the proxy middleware ends the response and does not "chain"
return renderAppToResponse(proxyResponse, request, serverResponse, renderer, config);
}
/**
* @param {string} reqPath
* @param {Request} req
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
function rewriteRequestPath(reqPath, req, config, parseRouteUrl) {
// the path comes in URL-encoded by default,
// but we don't want that because...
// 1. We need to URL-encode it before we send it out to the Layout Service, if it matches a route
// 2. We don't want to force people to URL-encode ignored routes, etc (just use spaces instead of %20, etc)
const decodedReqPath = decodeURIComponent(reqPath);
// if the request URL contains a path/route that should not be re-written, then just pass it along as-is
if (isUrlIgnored(reqPath, config)) {
// we do not return the decoded URL because we're using it verbatim - should be encoded.
return reqPath;
}
// if the request URL doesn't contain the layout service controller path, assume we need to rewrite the request URL so that it does
// if this seems redundant, it is. the config.pathRewriteExcludeRoutes should contain the layout service path, but can't always assume that it will...
if (decodedReqPath.indexOf(config.layoutServiceRoute) !== -1) {
return reqPath;
}
let finalReqPath = decodedReqPath;
const qsIndex = finalReqPath.indexOf('?');
let qs = '';
if (qsIndex > -1 || Object.keys(req.query).length) {
qs = (0, util_1.buildQueryString)(req.query);
// Splice qs part when url contains that
if (qsIndex > -1)
finalReqPath = finalReqPath.slice(0, qsIndex);
}
if (config.qsParams) {
if (qs) {
qs += '&';
}
qs += `${config.qsParams}`;
}
let lang;
if (parseRouteUrl) {
if (config.debug) {
console.log(`DEBUG: Parsing route URL using ${decodedReqPath} URL...`);
}
const routeParams = parseRouteUrl(finalReqPath);
if (routeParams) {
if (routeParams.sitecoreRoute) {
finalReqPath = routeParams.sitecoreRoute;
}
else {
finalReqPath = '/';
}
if (!finalReqPath.startsWith('/')) {
finalReqPath = `/${finalReqPath}`;
}
lang = routeParams.lang;
if (routeParams.qsParams) {
qs += `&${routeParams.qsParams}`;
}
if (config.debug) {
console.log('DEBUG: parseRouteUrl() result', routeParams);
}
}
}
let path = `${config.layoutServiceRoute}?item=${encodeURIComponent(finalReqPath)}&sc_apikey=${config.apiKey}`;
if (lang) {
path = `${path}&sc_lang=${lang}`;
}
if (qs) {
path = `${path}&${qs}`;
}
return path;
}
exports.rewriteRequestPath = rewriteRequestPath;
/**
* @param {string} originalUrl
* @param {ProxyConfig} config
* @param {boolean} noDebug
*/
function isUrlIgnored(originalUrl, config, noDebug = false) {
if (config.pathRewriteExcludePredicate && config.pathRewriteExcludeRoutes) {
console.error('ERROR: pathRewriteExcludePredicate and pathRewriteExcludeRoutes were both provided in config. Provide only one.');
process.exit(1);
}
let result = null;
if (config.pathRewriteExcludeRoutes) {
const matchRoute = decodeURIComponent(originalUrl).toUpperCase();
result = config.pathRewriteExcludeRoutes.find((excludedRoute) => excludedRoute.length > 0 && matchRoute.startsWith(excludedRoute));
if (!noDebug && config.debug) {
if (!result) {
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude list, and will be treated as a layout service route to render. Excludes:`, config.pathRewriteExcludeRoutes);
}
else {
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude list and will be served verbatim as received. Excludes: `, config.pathRewriteExcludeRoutes);
}
}
return result ? true : false;
}
if (config.pathRewriteExcludePredicate) {
result = config.pathRewriteExcludePredicate(originalUrl);
if (!noDebug && config.debug) {
if (!result) {
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude function, and will be treated as a layout service route to render.`);
}
else {
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude function and will be served verbatim as received.`);
}
}
return result;
}
return false;
}
/**
* @param {ClientRequest} proxyReq
* @param {Request} req
* @param {Response} res
* @param {ServerOptions} options
* @param {ProxyConfig} config
* @param {Function} customOnProxyReq
*/
function handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq) {
if (!isUrlIgnored(req.originalUrl, config, true)) {
// In case 'followRedirects' is enabled, and before the proxy was initialized we had set 'originalMethod'
// now we need to set req.method back to original one, since proxyReq is already initialized.
// See more info in 'preProxyHandler'
if (options.followRedirects && req.originalMethod === 'HEAD') {
req.method = req.originalMethod;
delete req.originalMethod;
if (config.debug) {
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers');
}
}
else if (proxyReq.method === 'HEAD') {
if (config.debug) {
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers');
}
// if a HEAD request, we still need to issue a GET so we can return accurate headers
proxyReq.method = 'GET';
}
}
// invoke custom onProxyReq
if (customOnProxyReq) {
customOnProxyReq(proxyReq, req, res, options);
}
}
/**
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
function createOptions(renderer, config, parseRouteUrl) {
var _a;
if (!config.maxResponseSizeBytes) {
config.maxResponseSizeBytes = 10 * 1024 * 1024;
}
// ensure all excludes are case insensitive
if (config.pathRewriteExcludeRoutes && Array.isArray(config.pathRewriteExcludeRoutes)) {
config.pathRewriteExcludeRoutes = config.pathRewriteExcludeRoutes.map((exclude) => exclude.toUpperCase());
}
if (config.debug) {
console.log('DEBUG: Final proxy config', config);
}
const customOnProxyReq = (_a = config.proxyOptions) === null || _a === void 0 ? void 0 : _a.onProxyReq;
return Object.assign(Object.assign({}, config.proxyOptions), { target: config.apiHost, changeOrigin: true, ws: config.ws || false, pathRewrite: (reqPath, req) => rewriteRequestPath(reqPath, req, config, parseRouteUrl), logLevel: config.debug ? 'debug' : 'info', onProxyReq: (proxyReq, req, res, options) => handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq), onProxyRes: (proxyRes, req, res) => handleProxyResponse(proxyRes, req, res, renderer, config) });
}
/**
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
function scProxy(renderer, config, parseRouteUrl) {
const options = createOptions(renderer, config, parseRouteUrl);
const preProxyHandler = (req, _res, next) => {
// When 'followRedirects' is enabled, 'onProxyReq' is executed after 'proxyReq' is initialized based on original 'req'
// and there are no public properties/methods to modify Redirectable 'proxyReq'.
// so, we need to set 'HEAD' req as 'GET' before the proxy is initialized.
// During the 'onProxyReq' event we will set 'req.method' back as 'HEAD'.
// if a HEAD request, we need to issue a GET so we can return accurate headers
if (req.method === 'HEAD' &&
options.followRedirects &&
!isUrlIgnored(req.originalUrl, config, true)) {
req.method = 'GET';
req.originalMethod = 'HEAD';
}
next();
};
return [preProxyHandler, (0, http_proxy_middleware_1.createProxyMiddleware)(options)];
}
exports.default = scProxy;

@@ -1,4 +0,441 @@

export * from './middleware';
export * from './types';
export * from './personalize';
export { GRAPHQL_LAYOUT_QUERY_NAME } from '@sitecore-jss/sitecore-jss/layout';
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { createProxyMiddleware } from 'http-proxy-middleware';
import HttpStatus from 'http-status-codes';
import setCookieParser from 'set-cookie-parser';
import zlib from 'zlib'; // node.js standard lib
import { buildQueryString, tryParseJson } from './util';
// For some reason, every other response returned by Sitecore contains the 'set-cookie' header with the SC_ANALYTICS_GLOBAL_COOKIE value as an empty string.
// This effectively sets the cookie to empty on the client as well, so if a user were to close their browser
// after one of these 'empty value' responses, they would not be tracked as a returning visitor after re-opening their browser.
// To address this, we simply parse the response cookies and if the analytics cookie is present but has an empty value, then we
// remove it from the response header. This means the existing cookie in the browser remains intact.
export const removeEmptyAnalyticsCookie = (proxyResponse) => {
const cookies = setCookieParser.parse(proxyResponse.headers['set-cookie']);
if (cookies) {
const analyticsCookieIndex = cookies.findIndex((c) => c.name === 'SC_ANALYTICS_GLOBAL_COOKIE');
if (analyticsCookieIndex !== -1) {
const analyticsCookie = cookies[analyticsCookieIndex];
if (analyticsCookie && analyticsCookie.value === '') {
cookies.splice(analyticsCookieIndex, 1);
/* eslint-disable no-param-reassign */
proxyResponse.headers['set-cookie'] = cookies;
/* eslint-enable no-param-reassign */
}
}
}
};
// inspired by: http://stackoverflow.com/a/22487927/9324
/**
* @param {IncomingMessage} proxyResponse
* @param {IncomingMessage} request
* @param {ServerResponse} serverResponse
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
*/
function renderAppToResponse(proxyResponse, request, serverResponse, renderer, config) {
return __awaiter(this, void 0, void 0, function* () {
// monkey-patch FTW?
const originalWriteHead = serverResponse.writeHead;
const originalWrite = serverResponse.write;
const originalEnd = serverResponse.end;
// these lines are necessary and must happen before we do any writing to the response
// otherwise the headers will have already been sent
delete proxyResponse.headers['content-length'];
proxyResponse.headers['content-type'] = 'text/html; charset=utf-8';
// remove IIS server header for security
delete proxyResponse.headers.server;
if (config.setHeaders) {
config.setHeaders(request, serverResponse, proxyResponse);
}
const contentEncoding = proxyResponse.headers['content-encoding'];
if (contentEncoding &&
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) {
delete proxyResponse.headers['content-encoding'];
}
// we are going to set our own status code if rendering fails
serverResponse.writeHead = () => serverResponse;
// buffer the response body as it is written for later processing
let buf = Buffer.from('');
serverResponse.write = (data, encoding) => {
if (Buffer.isBuffer(data)) {
buf = Buffer.concat([buf, data]); // append raw buffer
}
else {
buf = Buffer.concat([buf, Buffer.from(data, encoding)]); // append string with optional character encoding (default utf8)
}
// sanity check: if the response is huge, bail.
// ...we don't want to let someone bring down the server by filling up all our RAM.
if (buf.length > config.maxResponseSizeBytes) {
throw new Error('Document too large');
}
return true;
};
/**
* Extract layout service data from proxy response
*/
function extractLayoutServiceDataFromProxyResponse() {
return __awaiter(this, void 0, void 0, function* () {
if (proxyResponse.statusCode === HttpStatus.OK ||
proxyResponse.statusCode === HttpStatus.NOT_FOUND) {
let responseString;
if (contentEncoding &&
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) {
responseString = new Promise((resolve, reject) => {
if (config.debug) {
console.log('Layout service response is compressed; decompressing.');
}
zlib.unzip(buf, (error, result) => {
if (error) {
reject(error);
}
if (result) {
resolve(result.toString('utf-8'));
}
});
});
}
else {
responseString = Promise.resolve(buf.toString('utf-8'));
}
return responseString.then(tryParseJson);
}
return Promise.resolve(null);
});
}
/**
* function replies with HTTP 500 when an error occurs
* @param {Error} error
*/
function replyWithError(error) {
return __awaiter(this, void 0, void 0, function* () {
console.error(error);
let errorResponse = {
statusCode: proxyResponse.statusCode || HttpStatus.INTERNAL_SERVER_ERROR,
content: proxyResponse.statusMessage || 'Internal Server Error',
headers: {},
};
if (config.onError) {
const onError = yield config.onError(error, proxyResponse);
errorResponse = Object.assign(Object.assign({}, errorResponse), onError);
}
completeProxyResponse(Buffer.from(errorResponse.content), errorResponse.statusCode, errorResponse.headers);
});
}
// callback handles the result of server-side rendering
/**
* @param {Error | null} error
* @param {RenderResponse} result
*/
function handleRenderingResult(error, result) {
return __awaiter(this, void 0, void 0, function* () {
if (!error && !result) {
return replyWithError(new Error('Render function did not return a result or an error!'));
}
if (error) {
return replyWithError(error);
}
if (!result) {
// should not occur, but makes TS happy
return replyWithError(new Error('Render function result did not return a result.'));
}
if (!result.html) {
return replyWithError(new Error('Render function result was returned but html property was falsy.'));
}
if (config.transformSSRContent) {
result.html = yield config.transformSSRContent(result, request, serverResponse);
}
// we have to convert back to a buffer so that we can get the *byte count* (rather than character count) of the body
let content = Buffer.from(result.html);
// setting the content-length header is not absolutely necessary, but is recommended
proxyResponse.headers['content-length'] = content.length.toString(10);
// if original request was a HEAD, we should not return a response body
if (request.method === 'HEAD') {
if (config.debug) {
console.log('DEBUG: Original request method was HEAD, clearing response body');
}
content = Buffer.from([]);
}
if (result.redirect) {
if (!result.status) {
result.status = 302;
}
proxyResponse.headers.location = result.redirect;
}
const finalStatusCode = result.status || proxyResponse.statusCode || HttpStatus.OK;
if (config.debug) {
console.log('DEBUG: FINAL response headers for client', JSON.stringify(proxyResponse.headers, null, 2));
console.log('DEBUG: FINAL status code for client', finalStatusCode);
}
completeProxyResponse(content, finalStatusCode);
});
}
/**
* @param {Buffer | null} content
* @param {number} statusCode
* @param {IncomingHttpHeaders} [headers]
*/
function completeProxyResponse(content, statusCode, headers) {
if (!headers) {
headers = proxyResponse.headers;
}
originalWriteHead.apply(serverResponse, [statusCode, headers]);
if (content)
originalWrite.call(serverResponse, content);
originalEnd.call(serverResponse);
}
/**
* @param {object} layoutServiceData
*/
function createViewBag(layoutServiceData) {
return __awaiter(this, void 0, void 0, function* () {
let viewBag = {
statusCode: proxyResponse.statusCode,
dictionary: {},
};
if (config.createViewBag) {
const customViewBag = yield config.createViewBag(request, serverResponse, proxyResponse, layoutServiceData);
viewBag = Object.assign(Object.assign({}, viewBag), customViewBag);
}
return viewBag;
});
}
// as the response is ending, we parse the current response body which is JSON, then
// render the app using that JSON, but return HTML to the final response.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
serverResponse.end = () => __awaiter(this, void 0, void 0, function* () {
try {
const layoutServiceData = yield extractLayoutServiceDataFromProxyResponse();
const viewBag = yield createViewBag(layoutServiceData);
if (!layoutServiceData) {
throw new Error(`Received invalid response ${proxyResponse.statusCode} ${proxyResponse.statusMessage}`);
}
return renderer(handleRenderingResult,
// originalUrl not defined in `http-proxy-middleware` types but it exists
request.originalUrl, layoutServiceData, viewBag);
}
catch (error) {
return replyWithError(error);
}
});
});
}
/**
* @param {IncomingMessage} proxyResponse
* @param {Request} request
* @param {Response} serverResponse
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
*/
function handleProxyResponse(proxyResponse, request, serverResponse, renderer, config) {
removeEmptyAnalyticsCookie(proxyResponse);
if (config.debug) {
console.log('DEBUG: request url', request.url);
console.log('DEBUG: request query', request.query);
console.log('DEBUG: request original url', request.originalUrl);
console.log('DEBUG: proxied request response code', proxyResponse.statusCode);
console.log('DEBUG: RAW request headers', JSON.stringify(request.headers, null, 2));
console.log('DEBUG: RAW headers from the proxied response', JSON.stringify(proxyResponse.headers, null, 2));
}
// if the request URL contains any of the excluded rewrite routes, we assume the response does not need to be server rendered.
// instead, the response should just be relayed as usual.
if (isUrlIgnored(request.originalUrl, config, true)) {
return Promise.resolve(undefined);
}
// your first thought might be: why do we need to render the app here? why not just pass the JSON response to another piece of middleware that will render the app?
// the answer: the proxy middleware ends the response and does not "chain"
return renderAppToResponse(proxyResponse, request, serverResponse, renderer, config);
}
/**
* @param {string} reqPath
* @param {Request} req
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
export function rewriteRequestPath(reqPath, req, config, parseRouteUrl) {
// the path comes in URL-encoded by default,
// but we don't want that because...
// 1. We need to URL-encode it before we send it out to the Layout Service, if it matches a route
// 2. We don't want to force people to URL-encode ignored routes, etc (just use spaces instead of %20, etc)
const decodedReqPath = decodeURIComponent(reqPath);
// if the request URL contains a path/route that should not be re-written, then just pass it along as-is
if (isUrlIgnored(reqPath, config)) {
// we do not return the decoded URL because we're using it verbatim - should be encoded.
return reqPath;
}
// if the request URL doesn't contain the layout service controller path, assume we need to rewrite the request URL so that it does
// if this seems redundant, it is. the config.pathRewriteExcludeRoutes should contain the layout service path, but can't always assume that it will...
if (decodedReqPath.indexOf(config.layoutServiceRoute) !== -1) {
return reqPath;
}
let finalReqPath = decodedReqPath;
const qsIndex = finalReqPath.indexOf('?');
let qs = '';
if (qsIndex > -1 || Object.keys(req.query).length) {
qs = buildQueryString(req.query);
// Splice qs part when url contains that
if (qsIndex > -1)
finalReqPath = finalReqPath.slice(0, qsIndex);
}
if (config.qsParams) {
if (qs) {
qs += '&';
}
qs += `${config.qsParams}`;
}
let lang;
if (parseRouteUrl) {
if (config.debug) {
console.log(`DEBUG: Parsing route URL using ${decodedReqPath} URL...`);
}
const routeParams = parseRouteUrl(finalReqPath);
if (routeParams) {
if (routeParams.sitecoreRoute) {
finalReqPath = routeParams.sitecoreRoute;
}
else {
finalReqPath = '/';
}
if (!finalReqPath.startsWith('/')) {
finalReqPath = `/${finalReqPath}`;
}
lang = routeParams.lang;
if (routeParams.qsParams) {
qs += `&${routeParams.qsParams}`;
}
if (config.debug) {
console.log('DEBUG: parseRouteUrl() result', routeParams);
}
}
}
let path = `${config.layoutServiceRoute}?item=${encodeURIComponent(finalReqPath)}&sc_apikey=${config.apiKey}`;
if (lang) {
path = `${path}&sc_lang=${lang}`;
}
if (qs) {
path = `${path}&${qs}`;
}
return path;
}
/**
* @param {string} originalUrl
* @param {ProxyConfig} config
* @param {boolean} noDebug
*/
function isUrlIgnored(originalUrl, config, noDebug = false) {
if (config.pathRewriteExcludePredicate && config.pathRewriteExcludeRoutes) {
console.error('ERROR: pathRewriteExcludePredicate and pathRewriteExcludeRoutes were both provided in config. Provide only one.');
process.exit(1);
}
let result = null;
if (config.pathRewriteExcludeRoutes) {
const matchRoute = decodeURIComponent(originalUrl).toUpperCase();
result = config.pathRewriteExcludeRoutes.find((excludedRoute) => excludedRoute.length > 0 && matchRoute.startsWith(excludedRoute));
if (!noDebug && config.debug) {
if (!result) {
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude list, and will be treated as a layout service route to render. Excludes:`, config.pathRewriteExcludeRoutes);
}
else {
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude list and will be served verbatim as received. Excludes: `, config.pathRewriteExcludeRoutes);
}
}
return result ? true : false;
}
if (config.pathRewriteExcludePredicate) {
result = config.pathRewriteExcludePredicate(originalUrl);
if (!noDebug && config.debug) {
if (!result) {
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude function, and will be treated as a layout service route to render.`);
}
else {
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude function and will be served verbatim as received.`);
}
}
return result;
}
return false;
}
/**
* @param {ClientRequest} proxyReq
* @param {Request} req
* @param {Response} res
* @param {ServerOptions} options
* @param {ProxyConfig} config
* @param {Function} customOnProxyReq
*/
function handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq) {
if (!isUrlIgnored(req.originalUrl, config, true)) {
// In case 'followRedirects' is enabled, and before the proxy was initialized we had set 'originalMethod'
// now we need to set req.method back to original one, since proxyReq is already initialized.
// See more info in 'preProxyHandler'
if (options.followRedirects && req.originalMethod === 'HEAD') {
req.method = req.originalMethod;
delete req.originalMethod;
if (config.debug) {
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers');
}
}
else if (proxyReq.method === 'HEAD') {
if (config.debug) {
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers');
}
// if a HEAD request, we still need to issue a GET so we can return accurate headers
proxyReq.method = 'GET';
}
}
// invoke custom onProxyReq
if (customOnProxyReq) {
customOnProxyReq(proxyReq, req, res, options);
}
}
/**
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
function createOptions(renderer, config, parseRouteUrl) {
var _a;
if (!config.maxResponseSizeBytes) {
config.maxResponseSizeBytes = 10 * 1024 * 1024;
}
// ensure all excludes are case insensitive
if (config.pathRewriteExcludeRoutes && Array.isArray(config.pathRewriteExcludeRoutes)) {
config.pathRewriteExcludeRoutes = config.pathRewriteExcludeRoutes.map((exclude) => exclude.toUpperCase());
}
if (config.debug) {
console.log('DEBUG: Final proxy config', config);
}
const customOnProxyReq = (_a = config.proxyOptions) === null || _a === void 0 ? void 0 : _a.onProxyReq;
return Object.assign(Object.assign({}, config.proxyOptions), { target: config.apiHost, changeOrigin: true, ws: config.ws || false, pathRewrite: (reqPath, req) => rewriteRequestPath(reqPath, req, config, parseRouteUrl), logLevel: config.debug ? 'debug' : 'info', onProxyReq: (proxyReq, req, res, options) => handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq), onProxyRes: (proxyRes, req, res) => handleProxyResponse(proxyRes, req, res, renderer, config) });
}
/**
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
export default function scProxy(renderer, config, parseRouteUrl) {
const options = createOptions(renderer, config, parseRouteUrl);
const preProxyHandler = (req, _res, next) => {
// When 'followRedirects' is enabled, 'onProxyReq' is executed after 'proxyReq' is initialized based on original 'req'
// and there are no public properties/methods to modify Redirectable 'proxyReq'.
// so, we need to set 'HEAD' req as 'GET' before the proxy is initialized.
// During the 'onProxyReq' event we will set 'req.method' back as 'HEAD'.
// if a HEAD request, we need to issue a GET so we can return accurate headers
if (req.method === 'HEAD' &&
options.followRedirects &&
!isUrlIgnored(req.originalUrl, config, true)) {
req.method = 'GET';
req.originalMethod = 'HEAD';
}
next();
};
return [preProxyHandler, createProxyMiddleware(options)];
}

31

package.json
{
"name": "@sitecore-jss/sitecore-jss-proxy",
"version": "22.3.0-canary.15",
"description": "Middlewares, utilities to work in a headless mode",
"version": "22.3.0",
"description": "Proxy middleware for express.js server.",
"main": "dist/cjs/index.js",

@@ -12,9 +12,9 @@ "module": "dist/esm/index.js",

"lint": "eslint \"./src/**/*.ts\"",
"test": "mocha --require ts-node/register \"./src/**/*.test.ts\" --exit",
"test": "mocha --require ts-node/register \"./src/**/*.test.ts\"",
"prepublishOnly": "npm run build",
"coverage": "nyc npm test",
"generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --entryPoints src/personalize/index.ts --out ../../ref-docs/sitecore-jss-proxy src/index.ts --githubPages false"
"generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --out ../../ref-docs/sitecore-jss-proxy src/index.ts --githubPages false"
},
"engines": {
"node": ">=22"
"node": ">=20"
},

@@ -31,5 +31,2 @@ "author": {

"dependencies": {
"@sitecore-cloudsdk/core": "^0.4.1",
"@sitecore-cloudsdk/personalize": "^0.4.1",
"@sitecore-jss/sitecore-jss": "^22.3.0-canary.15",
"http-proxy-middleware": "^2.0.6",

@@ -43,24 +40,14 @@ "http-status-codes": "^2.2.0",

"@types/mocha": "^10.0.1",
"@types/node": "^22.9.0",
"@types/proxyquire": "^1.3.31",
"@types/node": "^20.14.2",
"@types/set-cookie-parser": "^2.4.2",
"@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2",
"chai": "^4.3.7",
"del-cli": "^5.0.0",
"eslint": "^8.56.0",
"express": "^4.19.2",
"eslint": "^8.33.0",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"proxyquire": "^2.1.3",
"sinon": "^17.0.1",
"supertest": "^7.0.0",
"ts-node": "^10.9.1",
"typescript": "~5.6.3"
"typescript": "~4.9.5"
},
"peerDependencies": {
"express": "^4.19.2"
},
"types": "types/index.d.ts",
"gitHead": "3b2f6e4ee89a36797e65b3c41b5cbc13cb78d38c",
"gitHead": "81b0239517966b6d0debdf6da3f12a98228c4d0c",
"files": [

@@ -67,0 +54,0 @@ "dist",

# Sitecore JavaScript Rendering SDK Proxy
This module is provided as a part of Sitecore JavaScript Rendering SDK (JSS). It provides middlewares, utilities to work in a headless mode.
This module is provided as a part of Sitecore JavaScript Rendering SDK (JSS). It contains the headless-mode SSR proxy implementation.

@@ -5,0 +5,0 @@ <!---

@@ -1,4 +0,21 @@

export * from './middleware';
export * from './types';
export * from './personalize';
export { GRAPHQL_LAYOUT_QUERY_NAME } from '@sitecore-jss/sitecore-jss/layout';
/// <reference types="qs" />
import { IncomingMessage } from 'http';
import { Request, RequestHandler } from 'express';
import { AppRenderer } from './AppRenderer';
import { ProxyConfig, ServerBundle } from './ProxyConfig';
import { RouteUrlParser } from './RouteUrlParser';
export declare const removeEmptyAnalyticsCookie: (proxyResponse: IncomingMessage) => void;
/**
* @param {string} reqPath
* @param {Request} req
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
export declare function rewriteRequestPath(reqPath: string, req: Request, config: ProxyConfig, parseRouteUrl?: RouteUrlParser): string;
/**
* @param {AppRenderer} renderer
* @param {ProxyConfig} config
* @param {RouteUrlParser} parseRouteUrl
*/
export default function scProxy(renderer: AppRenderer, config: ProxyConfig, parseRouteUrl: RouteUrlParser): RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>[];
export { ProxyConfig, ServerBundle };
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc