Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions packages/playground/personal-wp/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,50 @@
/>
</head>
<body>
<template id="load-error-fallback">
<!-- Use inline styles in case we are having trouble loading other resources -->
<div
style="
max-width: 520px;
margin: 80px auto;
font-family: system-ui, sans-serif;
text-align: center;
padding: 0 20px;
"
>
<h1 style="font-size: 1.4rem; margin-bottom: 12px">
Could not load Your WordPress
</h1>
<p style="line-height: 1.5">
The application failed to load. This is often caused by a
network issue.
</p>
<p style="line-height: 1.5; margin-top: 8px">
Check your internet connection, disable any VPN or ad
blocker that might interfere, and try again.
</p>
<button
onclick="location.reload()"
style="
margin-top: 15px;
margin-bottom: 15px;
font-size: 20px;
padding: 5px 10px;
cursor: pointer;
"
>
Try again
</button>
<p>
If the problem persists, please
<a
href="https://github.com/WordPress/wordpress-playground/issues/new"
target="_blank"
>report an issue on GitHub</a
>.
</p>
</div>
</template>
<!--
Shown before React loads when an iOS WKWebView (in-app browser) is
detected. The detection script below reveals this and skips the
Expand Down Expand Up @@ -250,7 +294,7 @@ <h1>Open in your browser</h1>
* Therefore, we're bypassing the cached import by adding a cache busting query parameter
* if the original import fails.
*/
import('./src/main').catch((e) => {
import('./src/main').catch(async (e) => {
console.error('Failed to load main module:', e);
try {
/**
Expand All @@ -273,14 +317,23 @@ <h1>Open in your browser</h1>
'_ts',
String(Date.now())
);
return import(
return await import(
/* @vite-ignore */ moduleUrl.toString()
);
}
} catch {
// Fall back to the original error
}
throw e;
/**
* If we get here, the app module completely failed to load.
* React is not available, so render a plain HTML
* error message.
*/
const root = document.getElementById('root');
const template = document.getElementById(
'load-error-fallback'
);
root.replaceChildren(template.content.cloneNode(true));
});
}
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export function getSiteErrorView(
// Show specific error views for certain error types, even if they occurred
// during a blueprint step. These errors have dedicated user-friendly views
// that provide better guidance than the generic step error view.
if (blueprintStepError && error !== 'network-firewall-interference') {
if (
blueprintStepError &&
error !== 'network-firewall-interference' &&
error !== 'resource-download-failed'
) {
return blueprintStepExecutionView(context);
}

Expand All @@ -55,6 +59,8 @@ export function getSiteErrorView(
return directoryHandleUnknownErrorView();
case 'network-firewall-interference':
return networkFirewallInterferenceView(context);
case 'resource-download-failed':
return resourceDownloadFailedView();
case 'site-boot-failed':
default:
return genericSiteBootFailedView(context);
Expand Down Expand Up @@ -461,6 +467,42 @@ function networkFirewallInterferenceView({
};
}

function resourceDownloadFailedView(): SiteErrorViewConfig {
return {
title: 'Could not download required files',
isDeveloperError: false,
hideReportButton: true,
detailSummaryOverride: 'Technical details',
body: (
<>
<p className={css.errorLead}>
Playground could not download one or more files it needs to
run. This is usually caused by a network problem.
</p>
<ul className={css.errorList}>
<li>Check your internet connection and try again.</li>
<li>
A firewall, proxy, or VPN may be blocking the download.
</li>
<li>
Browser extensions such as ad blockers can sometimes
interfere with downloads.
</li>
</ul>
</>
),
actions: [
<Button
variant="primary"
key="reload"
onClick={() => window.location.reload()}
>
Reload page
</Button>,
],
};
}

function genericSiteBootFailedView({
blueprintStepError,
helpers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import {
} from './slice-sites';
// @ts-ignore
import { corsProxyUrl } from 'virtual:cors-proxy-url';
import { findFirewallErrorInCauseChain } from './error-utils';
import {
findFirewallErrorInCauseChain,
findDownloadErrorInCauseChain,
} from './error-utils';
import {
initTabCoordinator,
checkForExistingTabs,
Expand Down Expand Up @@ -456,6 +459,13 @@ export function bootSiteClient(
details: firewallError,
})
);
} else if (findDownloadErrorInCauseChain(e)) {
dispatch(
setActiveSiteError({
error: 'resource-download-failed',
details: e,
})
);
} else {
dispatch(
setActiveSiteError({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { FirewallInterferenceError } from '@php-wasm/web-service-worker';

const MAX_CAUSE_CHAIN_DEPTH = 100;

/**
* Search through an error's cause chain to find a FirewallInterferenceError.
* Checks both instanceof and the error's name property to handle cases where
Expand All @@ -11,7 +13,15 @@ export function findFirewallErrorInCauseChain(
error: unknown
): FirewallInterferenceError | Error | undefined {
let current: unknown = error;
while (current) {
const seen = new Set<Error>();
let depth = 0;
while (current && depth < MAX_CAUSE_CHAIN_DEPTH) {
if (current instanceof Error) {
if (seen.has(current)) {
break;
}
seen.add(current);
}
if (current instanceof FirewallInterferenceError) {
return current;
}
Expand All @@ -23,6 +33,73 @@ export function findFirewallErrorInCauseChain(
}
current =
current instanceof Error ? (current as Error).cause : undefined;
depth++;
}
return undefined;
}

/**
* Known error message patterns that indicate a network/download failure.
* These cover fetch failures, dynamic import failures, and WebAssembly
* compile errors (which happen when a non-WASM response like an HTML
* error page is returned).
*/
const DOWNLOAD_ERROR_PATTERNS = [
// Standard fetch API failure
'Failed to fetch',
// Safari module import failure
'Importing a module script failed',
// Chrome/Firefox dynamic import failure
'error loading dynamically imported module',
// Firefox fetch failure
'NetworkError when attempting to fetch',
// Safari fetch failure
'Load failed',
];

/**
* Error class names that indicate a download/network problem.
* WebAssembly.CompileError and LinkError occur when the browser tries
* to compile a non-WASM response (e.g. an HTML error page) as WASM.
*/
const DOWNLOAD_ERROR_CLASS_NAMES = ['CompileError', 'LinkError'];

/**
* Search through an error's cause chain to find a network/download error.
* Checks error messages against known patterns and error class names
* against WebAssembly compilation errors.
*
* Handles both native Error objects and Comlink-serialized errors
* (which use `originalErrorClassName` instead of the native class name).
*
* Returns the matching error if found, or undefined if not.
*/
export function findDownloadErrorInCauseChain(
error: unknown
): Error | undefined {
let current: unknown = error;
const seen = new Set<Error>();
let depth = 0;
while (current && depth < MAX_CAUSE_CHAIN_DEPTH) {
if (current instanceof Error) {
if (seen.has(current)) {
break;
}
seen.add(current);
const message = current.message || '';
for (const pattern of DOWNLOAD_ERROR_PATTERNS) {
if (message.toLowerCase().includes(pattern.toLowerCase())) {
return current;
}
}
const className =
(current as any).originalErrorClassName || current.name;
if (className && DOWNLOAD_ERROR_CLASS_NAMES.includes(className)) {
return current;
}
}
current = current instanceof Error ? current.cause : undefined;
depth++;
}
return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type SiteError =
| 'blueprint-filesystem-required'
| 'blueprint-validation-failed'
| 'network-firewall-interference'
| 'resource-download-failed'
| 'tab-superseded';

export type SiteManagerSection = 'sidebar' | 'site-details' | 'blueprints';
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/remote/remote.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
const reportIssues = document.createElement('p');
reportIssues.innerHTML = `
If the problem persists, please
<a href="https://github.com/WordPress/playground-tools/issues/new"
<a href="https://github.com/WordPress/wordpress-playground/issues/new"
target="_blank"
>report an issue on GitHub</a>.
`;
Expand Down Expand Up @@ -213,7 +213,7 @@ <h3>Did you refuse to grant Playground storage access?</h3>
</p>
<p>
If neither method helped, please
<a href="https://github.com/WordPress/playground-tools/issues/new"
<a href="https://github.com/WordPress/wordpress-playground/issues/new"
target="_blank">
report an issue on GitHub
</a>.
Expand Down
58 changes: 55 additions & 3 deletions packages/playground/website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,50 @@
/>
</head>
<body>
<template id="load-error-fallback">
<!-- Use inline styles in case we are having trouble loading other resources -->
<div
style="
max-width: 520px;
margin: 80px auto;
font-family: system-ui, sans-serif;
text-align: center;
padding: 0 20px;
"
>
<h1 style="font-size: 1.4rem; margin-bottom: 12px">
Could not load WordPress Playground
</h1>
<p style="line-height: 1.5">
The application failed to load. This is often caused by a
network issue.
</p>
<p style="line-height: 1.5; margin-top: 8px">
Check your internet connection, disable any VPN or ad
blocker that might interfere, and try again.
</p>
<button
onclick="location.reload()"
style="
margin-top: 15px;
margin-bottom: 15px;
font-size: 20px;
padding: 5px 10px;
cursor: pointer;
"
>
Try again
</button>
<p>
If the problem persists, please
<a
href="https://github.com/WordPress/wordpress-playground/issues/new"
target="_blank"
>report an issue on GitHub</a
>.
</p>
</div>
</template>
<!--
Shown before React loads when an iOS WKWebView (in-app browser) is
detected. The detection script below reveals this and skips the
Expand Down Expand Up @@ -256,7 +300,7 @@ <h1>Open in your browser</h1>
* Therefore, we're bypassing the cached import by adding a cache busting query parameter
* if the original import fails.
*/
import('./src/main').catch((e) => {
import('./src/main').catch(async (e) => {
console.error('Failed to load main module:', e);
try {
/**
Expand All @@ -279,14 +323,22 @@ <h1>Open in your browser</h1>
'_ts',
String(Date.now())
);
return import(
return await import(
/* @vite-ignore */ moduleUrl.toString()
);
}
} catch {
// Fall back to the original error
}
throw e;
/**
* If we get here, the app module completely failed to load.
* React is not available, so render a plain HTML error message.
*/
const root = document.getElementById('root');
const template = document.getElementById(
'load-error-fallback'
);
root.replaceChildren(template.content.cloneNode(true));
});
}
</script>
Expand Down
Loading
Loading