Prerendering imposes its own (overridable) timeout with descriptive error

This commit is contained in:
SteveSandersonMS
2016-09-08 12:56:05 +01:00
parent 411100478a
commit 4ca1669db1
3 changed files with 47 additions and 5 deletions

View File

@@ -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

View File

@@ -23,7 +23,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
JavaScriptModuleExport bootModule,
string requestAbsoluteUrl,
string requestPathAndQuery,
object customDataParameter)
object customDataParameter,
int timeoutMilliseconds)
{
return nodeServices.InvokeExportAsync<RenderToStringResult>(
NodeScript.Value.FileName,
@@ -32,7 +33,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
bootModule,
requestAbsoluteUrl,
requestPathAndQuery,
customDataParameter);
customDataParameter,
timeoutMilliseconds);
}
}
}

View File

@@ -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<T>(promise: Promise<T>, timeoutMilliseconds: number, timeoutRejectionValue: any): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutTimer = setTimeout(() => {
reject(timeoutRejectionValue);
}, timeoutMilliseconds);
promise.then(
resolvedValue => {
clearTimeout(timeoutTimer);
resolve(resolvedValue);
},
rejectedValue => {
clearTimeout(timeoutTimer);
reject(rejectedValue);
}
)
});
}
function findBootModule<T>(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) {
const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName);
if (bootModule.webpackConfig) {