From 4ca1669db130970b7ce9f05d8e71de4e28e5a4e0 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 12:56:05 +0100 Subject: [PATCH] Prerendering imposes its own (overridable) timeout with descriptive error --- .../Prerendering/PrerenderTagHelper.cs | 7 +++- .../Prerendering/Prerenderer.cs | 6 ++- .../aspnet-prerendering/src/Prerendering.ts | 39 ++++++++++++++++++- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index b91c2a6..de02c90 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering private const string PrerenderExportAttributeName = "asp-prerender-export"; private const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config"; private const string PrerenderDataAttributeName = "asp-prerender-data"; + private const string PrerenderTimeoutAttributeName = "asp-prerender-timeout"; private static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI private readonly string _applicationBasePath; @@ -50,6 +51,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering [HtmlAttributeName(PrerenderDataAttributeName)] public object CustomDataParameter { get; set; } + [HtmlAttributeName(PrerenderTimeoutAttributeName)] + public int TimeoutMillisecondsParameter { get; set; } + [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } @@ -79,7 +83,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering }, unencodedAbsoluteUrl, unencodedPathAndQuery, - CustomDataParameter); + CustomDataParameter, + TimeoutMillisecondsParameter); output.Content.SetHtmlContent(result.Html); // Also attach any specified globals to the 'window' object. This is useful for transferring diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 40286d1..9d8724e 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -23,7 +23,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery, - object customDataParameter) + object customDataParameter, + int timeoutMilliseconds) { return nodeServices.InvokeExportAsync( NodeScript.Value.FileName, @@ -32,7 +33,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering bootModule, requestAbsoluteUrl, requestPathAndQuery, - customDataParameter); + customDataParameter, + timeoutMilliseconds); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts index 4d9269b..3a14f94 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts @@ -5,6 +5,8 @@ import * as domain from 'domain'; import { run as domainTaskRun } from 'domain-task/main'; import { baseUrl } from 'domain-task/fetch'; +const defaultTimeoutMilliseconds = 30 * 1000; + export interface RenderToStringCallback { (error: any, result: RenderToStringResult): void; } @@ -33,7 +35,7 @@ export interface BootModuleInfo { webpackConfig?: string; } -export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any) { +export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number) { findBootFunc(applicationBasePath, bootModule, (findBootFuncError, bootFunc) => { if (findBootFuncError) { callback(findBootFuncError, null); @@ -66,8 +68,22 @@ export function renderToString(callback: RenderToStringCallback, applicationBase // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context baseUrl(absoluteRequestUrl); + // Begin rendering, and apply a timeout + const bootFuncPromise = bootFunc(params); + if (!bootFuncPromise || typeof bootFuncPromise.then !== 'function') { + callback(`Prerendering failed because the boot function in ${bootModule.moduleName} did not return a promise.`, null); + return; + } + const timeoutMilliseconds = overrideTimeoutMilliseconds || defaultTimeoutMilliseconds; // e.g., pass -1 to override as 'never time out' + const bootFuncPromiseWithTimeout = timeoutMilliseconds > 0 + ? wrapWithTimeout(bootFuncPromise, timeoutMilliseconds, + `Prerendering timed out after ${timeoutMilliseconds}ms because the boot function in '${bootModule.moduleName}' ` + + 'returned a promise that did not resolve or reject. Make sure that your boot function always resolves or ' + + 'rejects its promise. You can change the timeout value using the \'asp-prerender-timeout\' tag helper.') + : bootFuncPromise; + // Actually perform the rendering - bootFunc(params).then(successResult => { + bootFuncPromiseWithTimeout.then(successResult => { callback(null, { html: successResult.html, globals: successResult.globals }); }, error => { callback(error, null); @@ -84,6 +100,25 @@ export function renderToString(callback: RenderToStringCallback, applicationBase }); } +function wrapWithTimeout(promise: Promise, timeoutMilliseconds: number, timeoutRejectionValue: any): Promise { + return new Promise((resolve, reject) => { + const timeoutTimer = setTimeout(() => { + reject(timeoutRejectionValue); + }, timeoutMilliseconds); + + promise.then( + resolvedValue => { + clearTimeout(timeoutTimer); + resolve(resolvedValue); + }, + rejectedValue => { + clearTimeout(timeoutTimer); + reject(rejectedValue); + } + ) + }); +} + function findBootModule(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) { const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName); if (bootModule.webpackConfig) {