Skip to content

Commit 1535f6f

Browse files
authored
fix: strip root hash from URL on error page (#907)
To not interfere with `CTRL+R`-style page reloads, when we there is an error to be shown, ensure we are on the bare URL and not `#/`. Fixes #817 though this was also partially handled by #906 and #902
1 parent e84baa7 commit 1535f6f

File tree

8 files changed

+72
-26
lines changed

8 files changed

+72
-26
lines changed

src/app.tsx

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,42 @@ function toAbsolutePath (path: string): string {
2828
return path
2929
}
3030

31+
/**
32+
* Do not redirect to `#/` if we are already on the bare domain, otherwise the
33+
* hash can interfere with CTRL+R-style page reloads
34+
*/
35+
function doNothingIfClickedOnRoot (event: React.MouseEvent): boolean {
36+
if (globalThis.location.hash === '') {
37+
event.preventDefault()
38+
event.stopPropagation()
39+
return false
40+
}
41+
42+
return true
43+
}
44+
3145
function Header (): React.ReactElement {
3246
let errorPageLink: React.ReactElement | undefined
3347

3448
if (globalThis.fetchError != null) {
3549
errorPageLink = (
36-
<NavLink to={`/${HASH_FRAGMENTS.IPFS_SW_FETCH_ERROR_UI}`} className={({ isActive }) => isActive ? 'white' : ''}>
50+
<NavLink to='/' className={({ isActive }) => (isActive ?? globalThis.location.hash === '') ? 'yellow-muted' : 'yellow'} onClickCapture={doNothingIfClickedOnRoot}>
3751
<FaExclamationCircle className='ml2 f3' />
3852
</NavLink>
3953
)
4054
}
4155

4256
if (globalThis.serverError != null) {
4357
errorPageLink = (
44-
<NavLink to={`/${HASH_FRAGMENTS.IPFS_SW_SERVER_ERROR_UI}`} className={({ isActive }) => isActive ? 'white' : ''}>
58+
<NavLink to='/' className={({ isActive }) => (isActive ?? globalThis.location.hash === '') ? 'red-muted' : 'red'} onClickCapture={doNothingIfClickedOnRoot}>
4559
<FaExclamationCircle className='ml2 f3' />
4660
</NavLink>
4761
)
4862
}
4963

5064
if (globalThis.originIsolationWarning != null) {
5165
errorPageLink = (
52-
<NavLink to={`/${HASH_FRAGMENTS.IPFS_SW_ORIGIN_ISOLATION_WARNING}`} className={({ isActive }) => isActive ? 'white' : ''}>
66+
<NavLink to='/' className={({ isActive }) => (isActive ?? globalThis.location.hash === '') ? 'yellow-muted' : 'yellow'} onClickCapture={doNothingIfClickedOnRoot}>
5367
<FaExclamationTriangle className='ml2 f3' />
5468
</NavLink>
5569
)
@@ -65,7 +79,7 @@ function Header (): React.ReactElement {
6579
<div className='pb1 ma0 mr2 inline-flex items-center aqua'>
6680
<h1 className='e2e-header-title f3 fw2 ttu sans-serif'>Service Worker Gateway</h1>
6781
{errorPageLink}
68-
<NavLink to='/' className={({ isActive }) => isActive ? 'white' : ''}>
82+
<NavLink to={`/${HASH_FRAGMENTS.IPFS_SW_LOAD_UI}`} className={({ isActive }) => (isActive || (errorPageLink == null && globalThis.location.hash === '')) ? 'white' : ''}>
6983
<FaDownload className='ml2 f3' />
7084
</NavLink>
7185
<NavLink to={`/${HASH_FRAGMENTS.IPFS_SW_ABOUT_UI}`} className={({ isActive }) => isActive ? 'white' : ''}>
@@ -83,31 +97,47 @@ function Header (): React.ReactElement {
8397
}
8498

8599
/**
86-
* This component is used when either:
87-
*
88-
* 1. the SW encountered an error fulfilling the request
89-
* 2. Installing the service worker failed
90-
* 3. The user wants to update the service worker config
91-
* 4. The user needs to accept the origin isolation warning
100+
* Dynamically create an index route - if an error has occurred, show that on
101+
* the landing page, otherwise show the "Enter a CID" UI page
92102
*/
93-
function App (): React.ReactElement {
103+
function getIndexRoute (): React.ReactElement {
94104
if (globalThis.fetchError != null && globalThis.location.hash === '') {
95-
window.location.hash = `/${HASH_FRAGMENTS.IPFS_SW_FETCH_ERROR_UI}`
105+
return (
106+
<Route element={<FetchErrorPage />} index />
107+
)
96108
}
97109

98110
if (globalThis.serverError != null && globalThis.location.hash === '') {
99-
window.location.hash = `/${HASH_FRAGMENTS.IPFS_SW_SERVER_ERROR_UI}`
111+
return (
112+
<Route element={<ServerErrorPage />} index />
113+
)
100114
}
101115

102116
if (globalThis.originIsolationWarning != null && globalThis.location.hash === '') {
103-
window.location.hash = `/${HASH_FRAGMENTS.IPFS_SW_ORIGIN_ISOLATION_WARNING}`
117+
return (
118+
<Route element={<OriginIsolationWarningPage />} index />
119+
)
104120
}
105121

122+
return (
123+
<Route element={<HomePage />} index />
124+
)
125+
}
126+
127+
/**
128+
* This component is used when either:
129+
*
130+
* 1. the SW encountered an error fulfilling the request
131+
* 2. Installing the service worker failed
132+
* 3. The user wants to update the service worker config
133+
* 4. The user needs to accept the origin isolation warning
134+
*/
135+
function App (): React.ReactElement {
106136
return (
107137
<HashRouter>
108138
<Header />
109139
<Routes>
110-
<Route index element={<HomePage />} />,
140+
{getIndexRoute()}
111141
<Route path={`/${HASH_FRAGMENTS.IPFS_SW_LOAD_UI}`} element={<HomePage />} />,
112142
<Route path={`/${HASH_FRAGMENTS.IPFS_SW_ABOUT_UI}`} element={<AboutPage />} />,
113143
<Route path={`/${HASH_FRAGMENTS.IPFS_SW_CONFIG_UI}`} element={<ConfigPage />} />,

src/lib/remove-root-hash.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Error pages live on the page root but React's HashRouter makes the page hash
3+
* "#/" which can interfere with CTRL+R-style page reloads so remove the hash
4+
* without causing a page reload so we are on a bare URL.
5+
*/
6+
export function removeRootHashIfPresent (): void {
7+
if (window.location.hash === '#/') {
8+
// const base = document.querySelector('base');
9+
// base?.setAttribute('href', '');
10+
// remove any UI-added navigation info
11+
history.pushState('', document.title, window.location.pathname + window.location.search)
12+
}
13+
}

src/pages/fetch-error.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ContentBox from '../components/content-box.jsx'
44
import { Link } from '../components/link.jsx'
55
import Terminal from '../components/terminal.jsx'
66
import { HASH_FRAGMENTS } from '../lib/constants.js'
7+
import { removeRootHashIfPresent } from '../lib/remove-root-hash.js'
78
import { toGatewayRoot } from '../lib/to-gateway-root.js'
89
import type { ConfigDb } from '../lib/config-db.js'
910
import type { RequestDetails, ResponseDetails } from '../sw/fetch-error-page.js'
@@ -167,6 +168,8 @@ export function FetchErrorPage ({ request, response, config, logs, providers }:
167168
)
168169
}
169170

171+
removeRootHashIfPresent()
172+
170173
const defaultShowDebugInfo = response.status === 500
171174

172175
let showDebugInfo = defaultShowDebugInfo

src/pages/home.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import { dnsLinkLabelDecoder, isInlinedDnsLink } from '../lib/dns-link-labels.js
66
import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.js'
77
import './default-page-styles.css'
88
import { pathRegex, subdomainRegex } from '../lib/regex.js'
9+
import { removeRootHashIfPresent } from '../lib/remove-root-hash.js'
910
import type { ReactElement } from 'react'
1011

1112
function LoadContent (): ReactElement {
13+
removeRootHashIfPresent()
14+
1215
let initialPath = localStorage.getItem(LOCAL_STORAGE_KEYS.forms.requestPath) ?? ''
1316

1417
if (initialPath === '') {

src/pages/origin-isolation-warning.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ServiceWorkerReadyButton } from '../components/sw-ready-button.jsx'
33
import { ConfigContext } from '../context/config-context.jsx'
44
import { QUERY_PARAMS } from '../lib/constants.js'
55
import './default-page-styles.css'
6+
import { removeRootHashIfPresent } from '../lib/remove-root-hash.js'
67
import { toGatewayRoot } from '../lib/to-gateway-root.js'
78
import type { ReactNode } from 'react'
89

@@ -55,6 +56,8 @@ export default function SubdomainWarningPage (): ReactNode {
5556
)
5657
}
5758

59+
removeRootHashIfPresent()
60+
5861
const [isSaving, setIsSaving] = useState(false)
5962
const configContext = useContext(ConfigContext)
6063

src/pages/server-error.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Button } from '../button.jsx'
33
import ContentBox from '../components/content-box.jsx'
44
import { Link } from '../components/link.jsx'
55
import Terminal from '../components/terminal.jsx'
6+
import { removeRootHashIfPresent } from '../lib/remove-root-hash.js'
67
import { toGatewayRoot } from '../lib/to-gateway-root.js'
78
import type { ReactElement } from 'react'
89

@@ -95,6 +96,8 @@ export function ServerErrorPage ({ url, error, title, logs }: ServerErrorPagePro
9596
)
9697
}
9798

99+
removeRootHashIfPresent()
100+
98101
function retry (): void {
99102
// remove any UI-added navigation info
100103
history.pushState('', document.title, window.location.pathname + window.location.search)

test-e2e/first-hit.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test.describe('first-hit ipfs-hosted', () => {
1818
}
1919
})
2020

21-
test('redirects are handled', async ({ page }) => {
21+
test('loads the index page from the root when an path is present', async ({ page }) => {
2222
const response = await page.goto('http://127.0.0.1:3334/ipfs/bafkqablimvwgy3y', {
2323
waitUntil: 'networkidle'
2424
})
@@ -27,9 +27,7 @@ test.describe('first-hit ipfs-hosted', () => {
2727
expect(response?.status()).toBe(200)
2828
const headers = await response?.allHeaders()
2929

30-
// we redirect to the root path with query param so sw can be registered at the root path
31-
await expect(page).toHaveURL('http://127.0.0.1:3334/ipfs/bafkqablimvwgy3y#/ipfs-sw-origin-isolation-warning')
32-
30+
// accept the warning
3331
await handleOriginIsolationWarning(page)
3432

3533
expect(headers?.['content-type']).toContain('text/html')

test-e2e/origin-isolation-warning.test.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { HASH_FRAGMENTS } from '../src/lib/constants.js'
21
import { testPathRouting as test, expect } from './fixtures/config-test-fixtures.js'
32
import { handleOriginIsolationWarning } from './fixtures/handle-origin-isolation-warning.js'
43

@@ -8,16 +7,10 @@ test.describe('origin isolation warning', () => {
87
waitUntil: 'networkidle'
98
})
109
const testUrl = 'http://127.0.0.1:3333/ipfs/bafkqablimvwgy3y'
11-
const testURL = new URL(testUrl)
1210
await page.goto(testUrl, {
1311
waitUntil: 'networkidle'
1412
})
1513

16-
const warningUrl = new URL(testURL)
17-
warningUrl.hash = `/${HASH_FRAGMENTS.IPFS_SW_ORIGIN_ISOLATION_WARNING}`
18-
19-
await expect(page).toHaveURL(warningUrl.toString())
20-
2114
// accept warning
2215
await handleOriginIsolationWarning(page)
2316

0 commit comments

Comments
 (0)