Skip to content

Commit

Permalink
feat: meaningful error pages (#195)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* chore: meaningful error page shows response details

* chore: refine meaningful error page

---------

Co-authored-by: Marcin Rataj <[email protected]>
  • Loading branch information
SgtPooki and lidel authored Apr 23, 2024
1 parent 40cc8c7 commit 80774f5
Showing 1 changed file with 122 additions and 1 deletion.
123 changes: 122 additions & 1 deletion src/sw.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<string, string>
status: number
statusText: string
url: string
}

/**
******************************************************
* "globals"
Expand Down Expand Up @@ -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[] = []
Expand All @@ -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<Response> {
const responseContentType = fetchResponse.headers.get('Content-Type')
let json: Record<string, any> | 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(`
<h1>Oops! Something went wrong inside of Service Worker IPFS Gateway.</h1>
<p><button onclick="window.location.reload(true);">Click here to retry</button></p>
<p>
<h2>Error details:</h2>
<p><b>Message: </b>${json.error.message}</p>
<b>Stacktrace: </b><pre>${json.error.stack}</pre>
</p>
<p>
<h2>Response details:</h2>
<h3>${responseDetails.status} ${responseDetails.statusText}</h3>
<pre>${JSON.stringify(responseDetails, null, 2)}</pre>
</p>
<p>
<h2>Service worker details:</h2>
<pre>${JSON.stringify(await getServiceWorkerDetails(), null, 2)}</pre>
</p>
`, {
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: new Headers({
'Content-Type': 'text/html'
})
})
}

/**
* TODO: more service worker details
*/
async function getServiceWorkerDetails (): Promise<ServiceWorkerDetails> {
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<boolean> {
firstInstallTime = firstInstallTime ?? await getInstallTimestamp()
const now = Date.now()
Expand Down

0 comments on commit 80774f5

Please sign in to comment.