• Jump To … +
    ClientController.js ClientRequest.js ExpressServerRequest.js ReactServerAgent.js Cache.js Plugins.js Request.js handlePage.js loggingClient.js ClientRequestSpec.js NormalValuesPage.js NullValuePromisesPage.js NullValuesPage.js reactMiddlewareSpec.js client.js common.js History.js RootContainer.js RootElement.js TheFold.js config.js constants.js Navigator.js RequestContext.js logging.js client.js common.js response.js server.js stats.js renderMiddleware.js server.js ClientCssHelper.js DebugUtil.js PageUtil.js RequestLocalStorage.js StringEscapeUtil.js bundleNameUtil.js navigateTo.js
  • renderMiddleware.js

  • ¶
    var logger = require('./logging').getLogger(__LOGGER__),
    	React = require('react'),
    	ReactDOMServer = require('react-dom/server'),
    	MobileDetect = require('mobile-detect'),
    	RequestContext = require('./context/RequestContext'),
    	RequestLocalStorage = require('./util/RequestLocalStorage'),
    	DebugUtil = require('./util/DebugUtil').default,
    	RLS = RequestLocalStorage.getNamespace(),
    	flab = require('flab'),
    	Q = require('q'),
    	config = require('./config'),
    	ExpressServerRequest = require("./ExpressServerRequest"),
    
    	PageUtil = require('./util/PageUtil'),
    	ReactServerAgent = require('./ReactServerAgent'),
    	StringEscapeUtil = require('./util/StringEscapeUtil'),
    	{getRootElementAttributes} = require('./components/RootElement'),
    	{PAGE_CSS_NODE_ID, PAGE_LINK_NODE_ID, PAGE_CONTENT_NODE_ID, PAGE_CONTAINER_NODE_ID} = require('./constants'),
    	{flushLogsToResponse} = require('./logging/response');
    
    var _ = {
    	map: require('lodash/map'),
    };
  • ¶

    TODO FIXME ?? It might be worthwhile to get rid of all the closure-y things in render() https://developers.google.com/speed/articles/optimizing-javascript

  • ¶

    If an element hasn’t rendered in this long it gets the axe.

    var FAILSAFE_RENDER_TIMEOUT = 20e3;
  • ¶

    If a page’s handleRoute fails to resolve this fast it gets the axe.

    var FAILSAFE_ROUTER_TIMEOUT = 20e3;
  • ¶

    We’ll use this for keeping track of request concurrency per worker.

    var ACTIVE_REQUESTS = 0;
  • ¶

    Some non-content items that can live in the elements array.

    var ELEMENT_PENDING         = -1;
    var ELEMENT_ALREADY_WRITTEN = -2;
    
    /**
     * renderMiddleware entrypoint. Called by express for every request.
     */
    module.exports = function(req, res, next, routes) {
    	RequestLocalStorage.startRequest(() => {
    		ACTIVE_REQUESTS++;
    
    		var start = RLS().startTime = new Date();
    		var startHR = process.hrtime();
    
    		logger.debug(`Incoming request for ${req.path}`);
    
    		initResponseCompletePromise(res);
  • ¶

    monkey-patch res.write so that we don’t try to write to the stream if it’s already closed

    		var origWrite = res.write;
    		res.write = function () {
    			if (!res.finished) {
    				origWrite.apply(res, arguments);
    			} else {
    				logger.error("Attempted write after response finished", { path: req && req.path || "unknown", stack: logger.stack() });
    			}
    		};
  • ¶

    TODO? pull this context building into its own middleware

    		var context = new RequestContext.Builder()
    			.setRoutes(routes)
    			.setDefaultXhrHeadersFromRequest(req)
    			.create({
  • ¶

    TODO: context opts?

    			});
  • ¶

    Need this stuff in for logging.

    		context.setServerStash({ req, res, start, startHR });
    
    		context.setDeviceType(getDeviceType(req));
    
    		var navigateDfd = Q.defer();
  • ¶

    setup navigation handler (TODO: should we have a ‘once’ version?)

    		context.onNavigate( (err, page) => {
    
    			if (!navigateDfd.promise.isPending()) {
    				logger.error("Finished navigation after FAILSAFE_ROUTER_TIMEOUT", {
    					page: context.page,
    					path: req.path,
    				});
    				return;
    			}
  • ¶

    Success.

    			navigateDfd.resolve();
    
    
    			if (err) {
  • ¶

    The page can elect to proceed to render even with a non-2xx response. If it doesn’t do so then we’re done.

    				var done = !(page && page.getHasDocument());
    
    				if (err.status === 301 || err.status === 302 || err.status === 307) {
    					if (done){
  • ¶

    This adds a boilerplate body.

    						res.redirect(err.status, err.redirectUrl);
    					} else {
  • ¶

    This expects our page to render a body. Hope they know what they’re doing.

    						res.set('Location', err.redirectUrl);
    					}
    				} else if (done) {
    					if (err.status === 404) {
    						next();
    					} else {
    						next(err);
    					}
    				}
    				if (done) {
    					logger.log("onNavigate received a non-2xx HTTP code", err);
    					handleResponseComplete(req, res, context, start, page);
    					return;
    				}
    			}
    			renderPage(req, res, context, start, page);
    
    		});
    
    
    		const timeout = setTimeout(navigateDfd.reject, FAILSAFE_ROUTER_TIMEOUT);
  • ¶

    Don’t leave dead timers hanging around.

    		navigateDfd.promise.then(() => clearTimeout(timeout));
  • ¶

    If we fail to navigate, we’ll throw a 500 and move on.

    		navigateDfd.promise.catch(() => {
    			logger.error("Failed to navigate after FAILSAFE_ROUTER_TIMEOUT", {
    				page: context.navigator.getCurrentRoute().name,
    				path: req.path,
    			});
    			handleResponseComplete(req, res, context, start, context.page);
    			next({status: 500});
    		});
    
    		context.navigate(new ExpressServerRequest(req));
    
    	});
    };
    
    module.exports.getActiveRequests = () => ACTIVE_REQUESTS;
    
    function initResponseCompletePromise(res){
    	var dfd = Q.defer();
    
    	res.on('close',  dfd.resolve);
    	res.on('finish', dfd.resolve);
    
    	RLS().responseCompletePromise = dfd.promise;
    }
    
    function handleResponseComplete(req, res, context, start, page) {
    
    	RLS().responseCompletePromise.then(RequestLocalStorage.bind(() => {
  • ¶

    All intentional response completion should funnel through this function. If this value starts climbing gradually that’s an indication that we have some unintentional response completion going on that we should deal with.

    		ACTIVE_REQUESTS--;
  • ¶

    Note that if the navigator couldn’t even map the request to a page, we won’t be able to call middleware handleComplete() here.

    		if (page) {
    			logRequestStats(req, res, context, start, page);
    
    			page.handleComplete();
    		}
    	}));
    }
    
    function renderPage(req, res, context, start, page) {
    
    	var routeName = context.navigator.getCurrentRoute().name;
    
    	logger.debug("Route Name: " + routeName);
    
    	var timer = logger.timer("lifecycle.individual");
  • ¶

    Protects some browsers (Chrome, IE) against MIME sniffing attacks. see: http://security.stackexchange.com/a/12916

    	res.set('X-Content-Type-Options', 'nosniff');
    
    	res.status(page.getStatus()||200);
  • ¶

    Handy to have random access to this rather than needing to thread it through everywhere.

    	RLS().page = page;
  • ¶

    Each of these functions has the same signature and returns a promise, so we can chain them up with a promise reduction.

    	var lifecycleMethods;
    	if (PageUtil.PageConfig.get('isFragment')){
    		lifecycleMethods = fragmentLifecycle();
    	} else if (PageUtil.PageConfig.get('isRawResponse')){
    		lifecycleMethods = rawResponseLifecycle();
    	} else if (req.query[ReactServerAgent.DATA_BUNDLE_PARAMETER]) {
    		lifecycleMethods = dataBundleLifecycle();
    	} else {
    		lifecycleMethods = pageLifecycle();
    	}
    
    	lifecycleMethods.reduce((chain, func) => chain
    		.then(() => func(req, res, context, start, page))
    		.then(() => {
    			timer.tick(func.name);
    			logger.time(`lifecycle.fromStart.${func.name}`, new Date - start);
    		})
    	).catch(err => {
    		logger.error("Error in renderPage chain", err)
  • ¶

    Register finish listener before ending response.

    		handleResponseComplete(req, res, context, start, page);
  • ¶

    Bummer.

    		res.status(500).end();
    	});
  • ¶

    TODO: we probably want a “we’re not waiting any longer for this” timeout as well, and cancel the waiting deferreds

    }
    
    function rawResponseLifecycle () {
    	return [
    		Q(), // NOOP lead-in to prime the reduction
    		setHttpHeaders,
    		setContentType,
    		writeResponseData,
    		handleResponseComplete,
    		endResponse,
    	];
    }
    
    function fragmentLifecycle () {
    	return [
    		Q(), // NOOP lead-in to prime the reduction
    		setHttpHeaders,
    		writeDebugComments,
    		writeBody,
    		handleResponseComplete,
    		endResponse,
    	];
    }
    
    function dataBundleLifecycle () {
    	return [
    		Q(), // NOOP lead-in to prime the reduction
    		setDataBundleContentType,
    		writeDataBundle,
    		handleResponseComplete,
    		endResponse,
    	];
    }
    
    function pageLifecycle() {
    	return [
    		Q(), // This is just a NOOP lead-in to prime the reduction.
    		setHttpHeaders,
    		writeHeader,
    		startBody,
    		writeBody,
    		wrapUpLateArrivals,
    		closeBody,
    		handleResponseComplete,
    		endResponse,
    	];
    }
    
    function setDataBundleContentType(req, res) {
    	res.set('Content-Type', 'application/json');
    }
    
    function setHttpHeaders(req, res, context, start, pageObject) {
  • ¶

    Write out custom page-defined http headers. Headers may be overwritten later on in the render chain (e.g. transfer encoding, content type)

    	const handler = header => res.set(header[0], header[1]);
    
    	return Q(pageObject.getHeaders()).then(headers => headers.forEach(handler));
    }
    
    function setContentType(req, res, context, start, pageObject) {
    	res.set('Content-Type', pageObject.getContentType());
    }
    
    function writeHeader(req, res, context, start, pageObject) {
  • ¶

    This is awkward and imprecise. We don’t want to put <script> tags between divs above the fold, so we’re going to keep separate track of time client and server side. Then we’ll put <noscript> tags with data elements representing offset from our server base time that we’ll apply to our client base time as a proxy for when the element arrived (when it’s actually when we sent it).

    	RLS().timingDataT0 = new Date;
    
    	res.type('html');
    	res.set('Transfer-Encoding', 'chunked');
    
    	res.write('<!DOCTYPE html><html lang="en"><head>');
  • ¶

    note: these responses can currently come back out-of-order, as many are returning promises. scripts and stylesheets are guaranteed

    	return Q.all([
    		renderDebugComments(pageObject, res),
    		renderTitle(pageObject, res),
  • ¶

    PLAT-602: inline scripts come before stylesheets because stylesheet downloads block inline script execution.

    		(pageObject.getJsBelowTheFold() && !pageObject.getSplitJsLoad())
    			? Q()
    			: renderScripts(pageObject, res),
    		renderStylesheets(pageObject, res)
    			.then(() => Q.all([
    				renderMetaTags(pageObject, res),
    				renderLinkTags(pageObject, res),
    				renderBaseTag(pageObject, res),
    			])),
    	]).then(() => {
  • ¶

    once we have finished rendering all of the pieces of the head element, we can close the head and start the body element.

    		res.write(`</head>`);
  • ¶

    Get headers out right away so secondary resource download can start.

    		flushRes(res);
    	});
    }
    
    function flushRes(res){
  • ¶

    This method is only defined on the response object if the compress middleware is installed, so we need to guard our calls.

    	if (res.flush) {
    		res.flush()
    		if (!RLS().didLogFirstFlush){
    			RLS().didLogFirstFlush = true;
    			logger.time('firstFlush', new Date - RLS().startTime);
    		}
    	}
    }
    
    function renderDebugComments (pageObject, res) {
    	var debugComments = pageObject.getDebugComments();
    	debugComments.map(debugComment => {
    		if (!debugComment.label || !debugComment.value) {
    			logger.warning("Debug comment is missing either a label or a value", debugComment);
    		}
    
    		res.write(`<!-- ${debugComment.label}: ${debugComment.value} -->`);
    	});
  • ¶

    resolve immediately.

    	return Q("");
    }
    
    function writeDebugComments (req, res, context, start, pageObject) {
    	return Q(renderDebugComments(pageObject, res));
    }
    
    function renderTitle (pageObject, res) {
    	return pageObject.getTitle().then((title) => {
    		res.write(`<title>${title}</title>`);
    	});
    }
    
    function attrfy (value) {
    	return value.replace(/"/g, '&quot;');
    }
    
    function renderMetaTags (pageObject, res) {
    	var metaTags = pageObject.getMetaTags();
    
    	var metaTagsRendered = metaTags.map(metaTagPromise => {
    		return metaTagPromise.then(PageUtil.makeArray).then(metaTags => metaTags.forEach(metaTag => {
    			if (metaTag) {
  • ¶

    TODO: escaping

    				if ((metaTag.name && metaTag.httpEquiv) || (metaTag.name && metaTag.charset) || (metaTag.charset && metaTag.httpEquiv)) {
    					throw new Error("Meta tag cannot have more than one of name, httpEquiv, and charset", metaTag);
    				}
    
    				if ((metaTag.name && !metaTag.content) || (metaTag.httpEquiv && !metaTag.content)) {
    					throw new Error("Meta tag has name or httpEquiv but does not have content", metaTag);
    				}
    
    				if (metaTag.noscript) res.write(`<noscript>`);
    				res.write(`<meta`);
    
    				if (metaTag.name) res.write(` name="${attrfy(metaTag.name)}"`);
    				if (metaTag.httpEquiv) res.write(` http-equiv="${attrfy(metaTag.httpEquiv)}"`);
    				if (metaTag.charset) res.write(` charset="${attrfy(metaTag.charset)}"`);
    				if (metaTag.property) res.write(` property="${attrfy(metaTag.property)}"`);
    				if (metaTag.content) res.write(` content="${attrfy(metaTag.content)}"`);
    
    				res.write(`>`)
    				if (metaTag.noscript) res.write(`</noscript>`);
    			}
    		}));
    	});
    
    	return Q.all(metaTagsRendered);
    }
    
    function renderLinkTags (pageObject, res) {
    	var linkTags = pageObject.getLinkTags();
    
    	var linkTagsRendered = linkTags.map(linkTagPromise => {
    		return linkTagPromise.then(PageUtil.makeArray).then(linkTags => linkTags.forEach(linkTag => {
    			if (linkTag) {
    				if (!linkTag.rel) {
    					throw new Error(`<link> tag specified without 'rel' attr`);
    				}
    
    				res.write(`<link ${PAGE_LINK_NODE_ID} ${
    					Object.keys(linkTag)
    						.map(attr => `${attr}="${attrfy(linkTag[attr])}"`)
    						.join(' ')
    				}>`);
    			}
    		}));
    	});
    
    	return Q.all(linkTagsRendered);
    }
    
    function renderBaseTag(pageObject, res) {
    	return pageObject.getBase().then((base) => {
    		if (base !== null) {
    			if (!base.href && !base.target) {
    				throw new Error("<base> needs at least one of 'href' or 'target'");
    			}
    			var tag = "<base";
    			if (base.href) {
    				tag += ` href="${attrfy(base.href)}"`;
    			}
    			if (base.target) {
    				tag += ` target="${attrfy(base.target)}"`;
    			}
    			tag += ">";
    			res.write(tag);
    		}
    	});
    }
    
    function renderScriptsSync(scripts, res) {
  • ¶

    right now, the getXXXScriptFiles methods return synchronously, no promises, so we can render immediately.

    	scripts.forEach( (script) => {
  • ¶

    make sure there’s a leading ‘/‘

    		if (!script.type) script.type = "text/javascript";
    
    		if (script.href) {
    			res.write(`<script src="${script.href}" type="${script.type}"></script>`);
    		} else if (script.text) {
    			res.write(`<script type="${script.type}">${script.text}</script>`);
    		} else {
    			throw new Error("Script cannot be rendered because it has neither an href nor a text attribute: " + script);
    		}
    	});
    }
    
    function renderScriptsAsync(scripts, res) {
  • ¶

    Nothing to do if there are no scripts.

    	if (!scripts.length) return;
  • ¶

    Don’t need “type” in