From 80774f50760e4b757dfacfb7d5df79f3fb020012 Mon Sep 17 00:00:00 2001
From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com>
Date: Tue, 23 Apr 2024 13:10:50 -0700
Subject: [PATCH] feat: meaningful error pages (#195)
* feat: basic error page handling on errors
* feat: sw info on error page
* chore: move comment
* fix: 301s do not result in error page
* chore: apply suggestions from code review
Co-authored-by: Marcin Rataj
* chore: meaningful error page shows response details
* chore: refine meaningful error page
---------
Co-authored-by: Marcin Rataj
---
src/sw.ts | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 122 insertions(+), 1 deletion(-)
diff --git a/src/sw.ts b/src/sw.ts
index 3776abee..3faa278f 100644
--- a/src/sw.ts
+++ b/src/sw.ts
@@ -1,7 +1,7 @@
import { createVerifiedFetch, type VerifiedFetch } from '@helia/verified-fetch'
import { dnsJsonOverHttps } from '@multiformats/dns/resolvers'
import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/channel.js'
-import { getConfig } from './lib/config-db.js'
+import { getConfig, type ConfigDb } from './lib/config-db.js'
import { contentTypeParser } from './lib/content-type-parser.js'
import { getRedirectUrl, isDeregisterRequest } from './lib/deregister-request.js'
import { GenericIDB } from './lib/generic-db.js'
@@ -46,6 +46,31 @@ interface LocalSwConfig {
installTimestamp: number
}
+/**
+ * When returning a meaningful error page, we provide the following details about
+ * the service worker
+ */
+interface ServiceWorkerDetails {
+ config: ConfigDb
+ crossOriginIsolated: boolean
+ installTime: string
+ origin: string
+ scope: string
+ state: string
+}
+
+/**
+ * When returning a meaningful error page, we provide the following details about
+ * the response that failed
+ */
+interface ResponseDetails {
+ responseBody: string
+ headers: Record
+ status: number
+ statusText: string
+ url: string
+}
+
/**
******************************************************
* "globals"
@@ -454,6 +479,10 @@ async function fetchHandler ({ path, request, event }: FetchHandlerArg): Promise
* the response object, regardless of it's inner content
*/
clearTimeout(signalAbortTimeout)
+ if (response.status >= 400) {
+ log.error('fetchHandler: response not ok: ', response)
+ return await errorPageResponse(response)
+ }
return response
} catch (err: unknown) {
const errorMessages: string[] = []
@@ -476,6 +505,98 @@ async function fetchHandler ({ path, request, event }: FetchHandlerArg): Promise
}
}
+/**
+ * TODO: better styling
+ * TODO: more error details from @helia/verified-fetch
+ */
+async function errorPageResponse (fetchResponse: Response): Promise {
+ const responseContentType = fetchResponse.headers.get('Content-Type')
+ let json: Record | null = null
+ let text: string | null = null
+ const responseBodyAsText: string | null = await fetchResponse.text()
+
+ if (responseContentType != null) {
+ if (responseContentType.includes('text/html')) {
+ return fetchResponse
+ } else if (responseContentType.includes('application/json')) {
+ // we may eventually provide error messaging from @helia/verified-fetch
+ try {
+ json = JSON.parse(responseBodyAsText)
+ } catch (e) {
+ text = responseBodyAsText
+ json = { error: { message: `${fetchResponse.statusText}: ${text}`, stack: null } }
+ }
+ }
+ }
+ if (json == null) {
+ text = await fetchResponse.text()
+ json = { error: { message: `${fetchResponse.statusText}: ${text}`, stack: null } }
+ }
+
+ const responseDetails = getResponseDetails(fetchResponse, responseBodyAsText)
+
+ /**
+ * TODO: output configuration
+ */
+ return new Response(`
+ Oops! Something went wrong inside of Service Worker IPFS Gateway.
+
+
+
Error details:
+ Message: ${json.error.message}
+ Stacktrace: ${json.error.stack}
+
+
+
Response details:
+ ${responseDetails.status} ${responseDetails.statusText}
+ ${JSON.stringify(responseDetails, null, 2)}
+
+
+
Service worker details:
+ ${JSON.stringify(await getServiceWorkerDetails(), null, 2)}
+
+ `, {
+ status: fetchResponse.status,
+ statusText: fetchResponse.statusText,
+ headers: new Headers({
+ 'Content-Type': 'text/html'
+ })
+ })
+}
+
+/**
+ * TODO: more service worker details
+ */
+async function getServiceWorkerDetails (): Promise {
+ const registration = self.registration
+ const state = registration.installing?.state ?? registration.waiting?.state ?? registration.active?.state ?? 'unknown'
+
+ return {
+ config: await getConfig(),
+ // TODO: implement https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy
+ crossOriginIsolated: self.crossOriginIsolated,
+ installTime: (new Date(firstInstallTime)).toUTCString(),
+ origin: self.location.origin,
+ scope: registration.scope,
+ state
+ }
+}
+
+function getResponseDetails (response: Response, responseBody: string): ResponseDetails {
+ const headers = {}
+ response.headers.forEach((value, key) => {
+ headers[key] = value
+ })
+
+ return {
+ responseBody,
+ headers,
+ status: response.status,
+ statusText: response.statusText,
+ url: response.url
+ }
+}
+
async function isTimebombExpired (): Promise {
firstInstallTime = firstInstallTime ?? await getInstallTimestamp()
const now = Date.now()