@autobusal/providers 1.29.4 → 1.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/Errors/BootstrapError.tsx +117 -6
- package/Queries/apiClient.ts +25 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.30.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Only the documents 403 redirects to the documents page.** This fired on
|
|
8
|
+
every 403 there is. The intent is sound - an account blocked by an unsigned
|
|
9
|
+
document should be taken to the screen that unblocks it - but the other
|
|
10
|
+
source of a 403 in this API is the email-verification gate, and sending
|
|
11
|
+
somebody there for that told them nothing and fixed nothing. It read as a
|
|
12
|
+
deliberate product redirect and was this line. Keyed on a `reason` the
|
|
13
|
+
server sets rather than the message, which is translated fifteen ways, and
|
|
14
|
+
guarded against redirecting the documents page to itself.
|
|
15
|
+
|
|
16
|
+
## 1.30.0
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`BootstrapError` tells a transient failure from a real one and recovers
|
|
21
|
+
by itself.** A rate limit or a dropped connection is not a system error,
|
|
22
|
+
and "something broke on our side" is wrong twice for either: it blames the
|
|
23
|
+
wrong thing, and it hands the visitor a reload button for a situation where
|
|
24
|
+
reloading immediately makes it worse. 429/503/504 and a request with no
|
|
25
|
+
response at all now get their own wording and an automatic retry that backs
|
|
26
|
+
off 5s, 10s, 20s, 30s.
|
|
27
|
+
|
|
3
28
|
## 1.29.4
|
|
4
29
|
|
|
5
30
|
### Added
|
|
@@ -35,11 +35,63 @@ interface Props {
|
|
|
35
35
|
* *into*. A full reload is the honest recovery, and it is what a visitor
|
|
36
36
|
* would do anyway.
|
|
37
37
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Is this a failure that fixes itself?
|
|
40
|
+
*
|
|
41
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-07
|
|
42
|
+
*
|
|
43
|
+
* A rate limit and a dropped connection are not system errors, and telling
|
|
44
|
+
* somebody "something broke on our side" for either is wrong twice over: it
|
|
45
|
+
* blames the wrong thing, and it hands them a reload button for a situation
|
|
46
|
+
* where reloading immediately is the one move that makes it worse.
|
|
47
|
+
*
|
|
48
|
+
* Read off the axios error rather than the message text, which is not
|
|
49
|
+
* stable. A 429 carries a status; a network failure carries none at all,
|
|
50
|
+
* which is what distinguishes it from a server that answered with a fault.
|
|
51
|
+
*/
|
|
52
|
+
const transient = (error: any): 'busy' | 'offline' | null => {
|
|
53
|
+
const status = error?.response?.status ?? error?.status;
|
|
54
|
+
|
|
55
|
+
if (status === 429) {
|
|
56
|
+
return 'busy';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (status === 503 || status === 504) {
|
|
60
|
+
return 'busy';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// No response at all - DNS, offline, connection refused, CORS preflight
|
|
64
|
+
// that never landed. Distinct from a server that replied with a 500.
|
|
65
|
+
if (error?.isAxiosError && !error?.response) {
|
|
66
|
+
return 'offline';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How long to wait before trying again.
|
|
74
|
+
*
|
|
75
|
+
* The rate limiter's window is a minute, so a few seconds is usually enough
|
|
76
|
+
* and a full minute feels broken. Backs off on each successive failure so a
|
|
77
|
+
* genuinely overloaded server is not hammered by every open tab at once.
|
|
78
|
+
*/
|
|
79
|
+
const DELAYS = [ 5, 10, 20, 30 ];
|
|
80
|
+
|
|
81
|
+
interface State {
|
|
82
|
+
failed: boolean
|
|
83
|
+
kind: 'busy' | 'offline' | null
|
|
84
|
+
attempt: number
|
|
85
|
+
countdown: number
|
|
86
|
+
}
|
|
40
87
|
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
class BootstrapError extends React.Component<Props, State> {
|
|
89
|
+
state: State = { failed: false, kind: null, attempt: 0, countdown: 0 };
|
|
90
|
+
|
|
91
|
+
private timer?: ReturnType<typeof setInterval>;
|
|
92
|
+
|
|
93
|
+
static getDerivedStateFromError(error: Error) {
|
|
94
|
+
return { failed: true, kind: transient(error) };
|
|
43
95
|
}
|
|
44
96
|
|
|
45
97
|
componentDidCatch(error: Error, info: React.ErrorInfo): void {
|
|
@@ -47,6 +99,45 @@ class BootstrapError extends React.Component<Props, { failed: boolean }> {
|
|
|
47
99
|
// knows the app failed to start, and a white page with a `log` entry is
|
|
48
100
|
// easy to miss when somebody finally does open the console.
|
|
49
101
|
console.error('[bootstrap] the application could not start', error, info);
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* A transient failure retries itself. The visitor did nothing wrong and
|
|
105
|
+
* has nothing useful to do, so asking them to press a button is asking
|
|
106
|
+
* them to do our waiting for us - and the thing they would press it for
|
|
107
|
+
* is a rate limit that clears on its own.
|
|
108
|
+
*/
|
|
109
|
+
if (transient(error)) {
|
|
110
|
+
this.countdownTo(this.state.attempt);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
componentWillUnmount(): void {
|
|
115
|
+
if (this.timer) {
|
|
116
|
+
clearInterval(this.timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private countdownTo(attempt: number): void {
|
|
121
|
+
const seconds = DELAYS[Math.min(attempt, DELAYS.length - 1)];
|
|
122
|
+
|
|
123
|
+
this.setState({ countdown: seconds });
|
|
124
|
+
|
|
125
|
+
this.timer = setInterval(() => {
|
|
126
|
+
this.setState(state => {
|
|
127
|
+
if (state.countdown > 1) {
|
|
128
|
+
return { ...state, countdown: state.countdown - 1 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
clearInterval(this.timer);
|
|
132
|
+
|
|
133
|
+
// A full reload rather than resetting this boundary: whatever
|
|
134
|
+
// suspended did so during bootstrap, so there is no reliable state
|
|
135
|
+
// to retry INTO - the same reasoning as the button below.
|
|
136
|
+
window.location.reload();
|
|
137
|
+
|
|
138
|
+
return { ...state, countdown: 0 };
|
|
139
|
+
});
|
|
140
|
+
}, 1000);
|
|
50
141
|
}
|
|
51
142
|
|
|
52
143
|
render() {
|
|
@@ -59,6 +150,26 @@ class BootstrapError extends React.Component<Props, { failed: boolean }> {
|
|
|
59
150
|
const language = getLanguage('en');
|
|
60
151
|
const copy = errors[language as keyof typeof errors] ?? errors.en;
|
|
61
152
|
|
|
153
|
+
/*
|
|
154
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-07
|
|
155
|
+
* A transient failure gets its own words and its own countdown. The
|
|
156
|
+
* generic system error keeps the reload button, because for a real
|
|
157
|
+
* fault there is nothing to wait for.
|
|
158
|
+
*/
|
|
159
|
+
const { kind, countdown } = this.state;
|
|
160
|
+
|
|
161
|
+
const title = kind === 'busy' ? (copy as any).busy ?? copy.system : copy.system;
|
|
162
|
+
|
|
163
|
+
const detail = kind === 'busy'
|
|
164
|
+
? ((copy as any).busy_detail ?? copy.system_detail)
|
|
165
|
+
: kind === 'offline'
|
|
166
|
+
? ((copy as any).offline_detail ?? copy.system_detail)
|
|
167
|
+
: copy.system_detail;
|
|
168
|
+
|
|
169
|
+
const message = kind
|
|
170
|
+
? String(detail).replace('{n}', String(countdown))
|
|
171
|
+
: detail;
|
|
172
|
+
|
|
62
173
|
return (
|
|
63
174
|
<div
|
|
64
175
|
role="alert"
|
|
@@ -75,11 +186,11 @@ class BootstrapError extends React.Component<Props, { failed: boolean }> {
|
|
|
75
186
|
>
|
|
76
187
|
<div style={ { maxWidth: '460px', textAlign: 'center' } }>
|
|
77
188
|
<h1 style={ { fontSize: '20px', lineHeight: 1.4, margin: '0 0 12px' } }>
|
|
78
|
-
{
|
|
189
|
+
{ title }
|
|
79
190
|
</h1>
|
|
80
191
|
|
|
81
192
|
<p style={ { fontSize: '15px', lineHeight: 1.6, margin: '0 0 24px', opacity: 0.8 } }>
|
|
82
|
-
{
|
|
193
|
+
{ message }
|
|
83
194
|
</p>
|
|
84
195
|
|
|
85
196
|
<button
|
package/Queries/apiClient.ts
CHANGED
|
@@ -88,8 +88,31 @@ apiClient.interceptors.response.use(response => (
|
|
|
88
88
|
return Promise.reject(error);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
/*
|
|
92
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-07
|
|
93
|
+
*
|
|
94
|
+
* Only the DOCUMENTS 403 goes to the documents page.
|
|
95
|
+
*
|
|
96
|
+
* This used to fire on every 403 there is. The intent is sound - an
|
|
97
|
+
* account blocked by an unsigned document should be taken to the one
|
|
98
|
+
* screen that unblocks it - but the other source of a 403 in this API is
|
|
99
|
+
* the email-verification gate, and sending somebody there for that told
|
|
100
|
+
* them nothing and fixed nothing. It read as a deliberate product
|
|
101
|
+
* redirect; it was this line.
|
|
102
|
+
*
|
|
103
|
+
* Keyed on a `reason` the server sets (Middleware\ValidUser), not on the
|
|
104
|
+
* message: that text is translated into fifteen languages, so comparing
|
|
105
|
+
* it would work in one.
|
|
106
|
+
*
|
|
107
|
+
* The pathname guard matters as much as the reason. Without it, a 403
|
|
108
|
+
* raised BY the documents page redirects the documents page to itself,
|
|
109
|
+
* which is a reload loop rather than a fix.
|
|
110
|
+
*/
|
|
111
|
+
if (
|
|
112
|
+
error.response.status === 403
|
|
113
|
+
&& error.response.data?.reason === 'documents'
|
|
114
|
+
&& window.location.pathname !== '/account/documents'
|
|
115
|
+
) {
|
|
93
116
|
window.location.href = '/account/documents';
|
|
94
117
|
}
|
|
95
118
|
|