diff --git a/src/uv.sw.js b/src/uv.sw.js index 6d5aec4..b36d74a 100644 --- a/src/uv.sw.js +++ b/src/uv.sw.js @@ -48,6 +48,11 @@ class UVServiceWorker extends Ultraviolet.EventEmitter { * @returns */ async fetch({ request }) { + /** + * @type {string|void} + */ + let fetchedURL; + try { if (!request.url.startsWith(location.origin + this.config.prefix)) return await fetch(request); @@ -116,23 +121,22 @@ class UVServiceWorker extends Ultraviolet.EventEmitter { if (reqEvent.intercepted) return reqEvent.returnValue; - const response = await this.bareClient.fetch( - requestCtx.blob - ? 'blob:' + location.origin + requestCtx.url.pathname - : requestCtx.url, - { - headers: requestCtx.headers, - method: requestCtx.method, - body: requestCtx.body, - credentials: requestCtx.credentials, - mode: - location.origin !== requestCtx.address.origin - ? 'cors' - : requestCtx.mode, - cache: requestCtx.cache, - redirect: requestCtx.redirect, - } - ); + fetchedURL = requestCtx.blob + ? 'blob:' + location.origin + requestCtx.url.pathname + : requestCtx.url; + + const response = await this.bareClient.fetch(fetchedURL, { + headers: requestCtx.headers, + method: requestCtx.method, + body: requestCtx.body, + credentials: requestCtx.credentials, + mode: + location.origin !== requestCtx.address.origin + ? 'cors' + : requestCtx.mode, + cache: requestCtx.cache, + redirect: requestCtx.redirect, + }); const responseCtx = new ResponseContext(requestCtx, response); const resEvent = new HookEvent(responseCtx, null, null); @@ -254,9 +258,7 @@ class UVServiceWorker extends Ultraviolet.EventEmitter { console.error(err); - return new Response(err.toString(), { - status: 500, - }); + return renderError(err, fetchedURL, this.address); } } static Ultraviolet = Ultraviolet; @@ -358,3 +360,185 @@ class HookEvent { this.#intercepted = true; } } + +/** + * + * @param {string} fetchedURL + * @param {string} bareServer + * @returns + */ +function hostnameErrorTemplate(fetchedURL, bareServer) { + const parsedFetchedURL = new URL(fetchedURL); + const script = + `remoteHostname.textContent = ${JSON.stringify( + parsedFetchedURL.hostname + )};` + + `bareServer.href = ${JSON.stringify(bareServer)};` + + `uvHostname.textContent = ${JSON.stringify(location.hostname)};` + + `reload.addEventListener("click", () => location.reload())`; + + return ( + '' + + '' + + '' + + "" + + 'Error' + + '' + + '' + + '

This site can’t be reached

' + + '
' + + '

’s server IP address could not be found.

' + + '

Try:

' + + '' + + '' + + `` + + '' + + '' + ); +} + +/** + * + * @param {string} title + * @param {string} code + * @param {string} id + * @param {string} message + * @param {string} trace + * @param {string} fetchedURL + * @param {string} bareServer + * @returns + */ +function errorTemplate( + title, + code, + id, + message, + trace, + fetchedURL, + bareServer +) { + // produced by bare-server-node + if (message === 'The specified host could not be resolved.') + return hostnameErrorTemplate(fetchedURL, bareServer); + + // turn script into a data URI so we don't have to escape any HTML values + const script = + `errorTitle.textContent = ${JSON.stringify(title)};` + + `errorCode.textContent = ${JSON.stringify(code)};` + + (id ? `errorId.textContent = ${JSON.stringify(id)};` : '') + + `errorMessage.textContent = ${JSON.stringify(message)};` + + `errorTrace.value = ${JSON.stringify(trace)};` + + `fetchedURL.textContent = ${JSON.stringify(fetchedURL)};` + + `bareServer.href = ${JSON.stringify(bareServer)};` + + `uvHostname.textContent = ${JSON.stringify(location.hostname)};` + + `reload.addEventListener("click", () => location.reload());`; + + return ( + '' + + '' + + '' + + "" + + 'Error' + + '' + + '' + + "

" + + '
' + + '

Failed to load

' + + '

' + + '' + + '' + + (id ? '' : '') + + '
Code:
ID:
' + + '' + + '

Try:

' + + '' + + '' + + `` + + '' + + '' + ); +} + +/** + * @typedef {import("@tomphttp/bare-client").BareError} BareError + */ + +/** + * + * @param {unknown} err + * @returns {err is BareError} + */ +function isBareError(err) { + return err instanceof Error && typeof err.body === 'object'; +} + +/** + * + * @param {unknown} err + * @param {string} fetchedURL + * @param {string} bareServer + */ +function renderError(err, fetchedURL, bareServer) { + /** + * @type {number} + */ + let status; + /** + * @type {string} + */ + let title; + /** + * @type {string} + */ + let code; + let id = ''; + /** + * @type {string} + */ + let message; + + if (isBareError(err)) { + status = err.status; + title = 'Error communicating with the Bare server'; + message = err.body.message; + code = err.body.code; + id = err.body.id; + } else { + status = 500; + title = 'Error processing your request'; + message = 'Internal Server Error'; + code = err instanceof Error ? err.name : 'UNKNOWN'; + } + + return new Response( + errorTemplate( + title, + code, + id, + message, + String(err), + fetchedURL, + bareServer + ), + { + status, + headers: { + 'content-type': 'text/html', + }, + } + ); +}