Skip to content

Commit 4edcee6

Browse files
committed
fix: add error boundary, safe base64 encoding, and proper fetch error handling
1 parent 5b79fb7 commit 4edcee6

4 files changed

Lines changed: 119 additions & 27 deletions

File tree

src/component/ErrorBoundary.jsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
3+
const crashStyle = {
4+
minHeight: '100vh',
5+
display: 'flex',
6+
alignItems: 'center',
7+
justifyContent: 'center',
8+
padding: '24px',
9+
backgroundColor: 'var(--bg-primary)',
10+
color: 'var(--text-primary)',
11+
textAlign: 'center',
12+
};
13+
14+
class ErrorBoundary extends React.Component {
15+
constructor(props) {
16+
super(props);
17+
this.state = { hasError: false };
18+
}
19+
20+
static getDerivedStateFromError() {
21+
return { hasError: true };
22+
}
23+
24+
render() {
25+
const { hasError } = this.state;
26+
const { children } = this.props;
27+
28+
if (hasError) {
29+
return (
30+
<div style={crashStyle}>
31+
<div>
32+
<h2 style={{ marginBottom: '8px' }}>Something went wrong</h2>
33+
<p style={{ marginBottom: '16px', opacity: 0.9 }}>
34+
The app hit an unexpected error. Reload to continue.
35+
</p>
36+
<button
37+
type="button"
38+
className="btn btn-primary"
39+
onClick={() => window.location.reload()}
40+
>
41+
Reload
42+
</button>
43+
</div>
44+
</div>
45+
);
46+
}
47+
48+
return children;
49+
}
50+
}
51+
52+
export default ErrorBoundary;

src/graph-builder/local-storage-manager.js

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
import { toast } from 'react-toastify';
22

3+
const encodeBase64 = (value) => {
4+
const bytes = new TextEncoder().encode(value);
5+
let binary = '';
6+
bytes.forEach((byte) => {
7+
binary += String.fromCharCode(byte);
8+
});
9+
return window.btoa(binary);
10+
};
11+
12+
const decodeBase64 = (value) => {
13+
const binary = window.atob(value);
14+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
15+
return new TextDecoder().decode(bytes);
16+
};
17+
18+
const parseStoredJson = (raw) => {
19+
try {
20+
return JSON.parse(decodeBase64(raw));
21+
} catch (e) {
22+
return null;
23+
}
24+
};
25+
326
const localStorageGet = (key) => {
427
try {
528
return window.localStorage.getItem(key);
@@ -27,11 +50,16 @@ const localStorageRemove = (key) => {
2750

2851
const getSet = (ALL_GRAPHS) => {
2952
if (!localStorageGet(ALL_GRAPHS)) {
30-
localStorageSet(ALL_GRAPHS, window.btoa(JSON.stringify([])));
53+
localStorageSet(ALL_GRAPHS, encodeBase64(JSON.stringify([])));
3154
}
3255
const raw = localStorageGet(ALL_GRAPHS);
3356
if (!raw) return new Set();
34-
return new Set(JSON.parse(window.atob(raw)));
57+
const parsed = parseStoredJson(raw);
58+
if (!Array.isArray(parsed)) {
59+
localStorageSet(ALL_GRAPHS, encodeBase64(JSON.stringify([])));
60+
return new Set();
61+
}
62+
return new Set(parsed);
3563
};
3664

3765
const localStorageManager = {
@@ -41,24 +69,29 @@ const localStorageManager = {
4169
allgs: getSet(window.btoa('ALL_GRAPHS')),
4270

4371
saveAllgs() {
44-
localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify(Array.from(this.allgs))));
72+
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify(Array.from(this.allgs))));
4573
},
4674

4775
addEmptyIfNot() {
4876
if (!localStorageGet(this.ALL_GRAPHS)) {
49-
localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify([])));
77+
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify([])));
5078
}
5179
},
5280

5381
get(id) {
5482
const raw = localStorageGet(id);
5583
if (raw === null) return null;
56-
return JSON.parse(window.atob(raw));
84+
const parsed = parseStoredJson(raw);
85+
if (parsed === null) {
86+
localStorageRemove(id);
87+
return null;
88+
}
89+
return parsed;
5790
},
5891
save(id, graphContent) {
5992
this.addGraph(id);
6093
const serializedJson = JSON.stringify(graphContent);
61-
localStorageSet(id, window.btoa(serializedJson));
94+
localStorageSet(id, encodeBase64(serializedJson));
6295
},
6396
remove(id) {
6497
if (this.allgs.delete(id)) this.saveAllgs();
@@ -72,16 +105,25 @@ const localStorageManager = {
72105
getAllGraphs() {
73106
const raw = localStorageGet(this.ALL_GRAPHS);
74107
if (!raw) return [];
75-
return JSON.parse(window.atob(raw));
108+
const parsed = parseStoredJson(raw);
109+
if (!Array.isArray(parsed)) {
110+
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify([])));
111+
return [];
112+
}
113+
return parsed;
76114
},
77115
addToFront(id) {
78116
if (this.allgs.has(id)) return;
79117
this.allgs.add(id);
80118
const raw = localStorageGet(this.ALL_GRAPHS);
81119
if (!raw) return;
82-
const Garr = JSON.parse(window.atob(raw));
120+
const Garr = parseStoredJson(raw);
121+
if (!Array.isArray(Garr)) {
122+
this.saveAllgs();
123+
return;
124+
}
83125
Garr.unshift(id);
84-
localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify(Garr)));
126+
localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify(Garr)));
85127
},
86128
getAuthorName() {
87129
return localStorageGet(this.AUTHOR_NAME) || '';

src/index.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import React from 'react';
22
import ReactDOM from 'react-dom';
33
import './index.css';
44
import App from './App';
5+
import ErrorBoundary from './component/ErrorBoundary';
56
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
67
import reportWebVitals from './reportWebVitals';
78

89
ReactDOM.render(
910
<React.StrictMode>
10-
<App />
11+
<ErrorBoundary>
12+
<App />
13+
</ErrorBoundary>
1114
</React.StrictMode>,
1215
document.getElementById('root'),
1316
);

src/serverCon/crud_http.js

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import ec from './config';
22

3+
function readTextOrThrow(x) {
4+
return x.text().then((text) => {
5+
if (x.ok) return text;
6+
throw new Error(text || `Request failed with status ${x.status}`);
7+
});
8+
}
9+
310
function getGraph(serverID) {
4-
return fetch(`${ec.baseURL + ec.getGraph(serverID)}`).then((x) => x.text());
11+
return fetch(`${ec.baseURL + ec.getGraph(serverID)}`).then((x) => readTextOrThrow(x));
512
}
613

714
function getGraphWithHashCheck(serverID, latestHash) {
815
return fetch(`${ec.baseURL + ec.getGraph(serverID)}`, {
916
headers: {
1017
'X-Latest-Hash': latestHash,
1118
},
12-
}).then((x) => {
13-
if (x.status === 200) return x.text();
14-
return Promise.reject(x.text());
15-
});
19+
}).then((x) => readTextOrThrow(x));
1620
}
1721

1822
function postGraph(graphml) {
@@ -22,10 +26,7 @@ function postGraph(graphml) {
2226
},
2327
method: 'POST',
2428
body: graphml,
25-
}).then((x) => {
26-
if (!x.ok) return Promise.reject(x.text());
27-
return x.text();
28-
});
29+
}).then((x) => readTextOrThrow(x));
2930
}
3031

3132
function updateGraph(serverID, graphml) {
@@ -35,10 +36,7 @@ function updateGraph(serverID, graphml) {
3536
'Content-Type': 'application/xml',
3637
},
3738
body: graphml,
38-
}).then((x) => {
39-
if (!x.ok) return Promise.reject(x.text());
40-
return x.text();
41-
});
39+
}).then((x) => readTextOrThrow(x));
4240
}
4341

4442
function forceUpdateGraph(serverID, graphml) {
@@ -48,10 +46,7 @@ function forceUpdateGraph(serverID, graphml) {
4846
'Content-Type': 'application/xml',
4947
},
5048
body: graphml,
51-
}).then((x) => {
52-
if (!x.ok) return Promise.reject(x.text());
53-
return x.text();
54-
});
49+
}).then((x) => readTextOrThrow(x));
5550
}
5651

5752
export {

0 commit comments

Comments
 (0)