Skip to content

Commit b8b2be7

Browse files
committed
Be less aggressive about restarts due to server disconnection
1 parent a8ae365 commit b8b2be7

File tree

7 files changed

+152
-21
lines changed

7 files changed

+152
-21
lines changed

src/components/app.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { PlanPicker } from './account/plan-picker';
3434
import { ModalOverlay } from './account/modal-overlay';
3535
import { CheckoutSpinner } from './account/checkout-spinner';
3636
import { HtmlContextMenu } from './html-context-menu';
37+
import { DisconnectedWarning } from './disconnected-warning';
3738

3839
const AppContainer = styled.div<{ inert?: boolean }>`
3940
display: flex;
@@ -229,6 +230,8 @@ class App extends React.Component<{
229230
<Route path={'/send'} pageComponent={SendPage} />
230231
<Route path={'/settings'} pageComponent={SettingsPage} />
231232
</Router>
233+
234+
<DisconnectedWarning />
232235
</AppContainer>
233236

234237
{ !!modal && <ModalOverlay /> }
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as React from 'react';
2+
import { inject, observer } from 'mobx-react';
3+
4+
import { styled } from '../styles';
5+
import { Icon } from '../icons';
6+
7+
import { ProxyStore } from '../model/proxy-store';
8+
import { canRestartApp, restartApp } from '../services/desktop-api';
9+
10+
import { Button } from './common/inputs';
11+
import { trackEvent } from '../metrics';
12+
import { logError } from '../errors';
13+
14+
const WarningContainer = styled.div`
15+
position: absolute;
16+
bottom: 40px;
17+
right: 40px;
18+
max-width: 300px;
19+
20+
background-color: ${p => p.theme.mainBackground};
21+
color: ${p => p.theme.mainColor};
22+
border: 2px solid ${p => p.theme.warningColor};
23+
box-shadow: 0 2px 10px 5px rgba(0,0,0,${p => p.theme.boxShadowAlpha});
24+
border-radius: 4px;
25+
26+
z-index: 1000;
27+
padding: 15px 15px;
28+
line-height: 1.4;
29+
30+
display: flex;
31+
gap: 10px;
32+
flex-direction: column;
33+
`;
34+
35+
const RestartButton = styled(Button)`
36+
&:not(:disabled) {
37+
background-color: ${p => p.theme.popColor};
38+
font-weight: bold;
39+
padding: 10px 15px;
40+
41+
display: flex;
42+
align-items: center;
43+
justify-content: center;
44+
gap: 15px;
45+
}
46+
`;
47+
48+
export const DisconnectedWarning = inject('proxyStore')(observer((props: {
49+
proxyStore?: ProxyStore
50+
}) => {
51+
if (!props.proxyStore?.streamDisconnected) return null;
52+
53+
return (
54+
<WarningContainer>
55+
<p>
56+
<strong>Disconnected from internal proxy server</strong>
57+
</p>
58+
59+
<p>
60+
Please wait a moment for reconnection...
61+
</p>
62+
63+
<p>
64+
In the meantime, some features may be unavailable. If this doesn't resolve
65+
quickly, or happens frequently, please <a
66+
href="https://github.com/httptoolkit/httptoolkit/issues/new?template=bug.yml"
67+
target="_blank"
68+
rel="noreferrer"
69+
>report a bug</a> and share as much information as possible to help
70+
get this fixed.
71+
</p>
72+
73+
{ canRestartApp()
74+
? <>
75+
<p>
76+
Restart the app below to fix this automatically:
77+
</p>
78+
79+
<RestartButton
80+
onClick={() => {
81+
logError('Manual restart required to fix server disconnect');
82+
setTimeout(() => restartApp(), 100); // Brief delay for logging
83+
}}
84+
>
85+
<Icon icon='Repeat' /> Restart App
86+
</RestartButton>
87+
</>
88+
: <p>
89+
If this problem persists, please restart the application to reinitialize
90+
the proxy server.
91+
</p>
92+
}
93+
</WarningContainer>
94+
);
95+
}));

src/icons.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
Sun,
2424
Moon,
2525
CircleHalf,
26-
Swatches
26+
Swatches,
27+
Repeat
2728
} from '@phosphor-icons/react';
2829

2930
export type IconKey = keyof typeof Icons;
@@ -55,7 +56,8 @@ const Icons = {
5556
Sun,
5657
Moon,
5758
CircleHalf,
58-
Swatches
59+
Swatches,
60+
Repeat
5961
} as const;
6062

6163
// Import required FA icons:

src/model/events/events-store.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as _ from 'lodash';
22
import {
33
observable,
4-
action,
5-
computed,
4+
action
65
} from 'mobx';
76
import { HarParseError } from 'har-validator';
87

src/model/interception/interceptor-store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { observable, runInAction, flow, action } from "mobx";
1+
import { observable, runInAction, action } from "mobx";
22

33
import { lazyObservablePromise } from "../../util/observable";
44

src/model/proxy-store.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ export class ProxyStore {
173173
http: any,
174174
webrtc: any
175175
}>({
176-
adminServerUrl: 'http://127.0.0.1:45456'
176+
adminServerUrl: 'http://127.0.0.1:45456',
177+
adminStreamReconnectAttempts: Infinity
177178
});
178179

179180
// These are persisted initially, so we know if the user updates them that we
@@ -237,7 +238,29 @@ export class ProxyStore {
237238
});
238239
});
239240

240-
private monitorRemoteClientConnection(client: PluggableAdmin.AdminClient<{}>) {
241+
@observable
242+
streamDisconnected: boolean = false;
243+
244+
private async monitorRemoteClientConnection(client: PluggableAdmin.AdminClient<{}>) {
245+
// Track stream connect/disconnected state:
246+
client.on('stream-reconnecting', action(() => {
247+
console.log('Admin client stream reconnecting...');
248+
this.streamDisconnected = true;
249+
}));
250+
251+
client.on('stream-reconnected', action(() => {
252+
console.log('Admin client reconnected');
253+
this.streamDisconnected = false;
254+
}));
255+
256+
// We show the below as disconnection, but we generally won't recover - this
257+
// probably means the server has unexpectedly cleanly shut down.
258+
client.on('stopped', action(() => {
259+
console.log('Server stopped');
260+
this.streamDisconnected = true;
261+
}));
262+
263+
// Log various other related events for debugging:
241264
client.on('stream-error', (err) => {
242265
console.log('Admin client stream error', err);
243266
});
@@ -246,19 +269,6 @@ export class ProxyStore {
246269
});
247270
client.on('stream-reconnect-failed', (err) => {
248271
logError(err.message ? err : new Error('Client reconnect error'), { cause: err });
249-
250-
alert("Server disconnected unexpectedly, app restart required.\n\nPlease report this at github.com/httptoolkit/httptoolkit.");
251-
setTimeout(() => { // Tiny wait for any other UI events to fire (error reporting/logging/other UI responsiveness)
252-
if (DesktopApi.restartApp) {
253-
// Where possible (recent desktop release) we restart the whole app directly
254-
DesktopApi.restartApp();
255-
} else if (!navigator.platform?.startsWith('Mac')) {
256-
// If not, on Windows & Linux we just close the window (which restarts)
257-
window.close();
258-
}
259-
// On Mac, app exit is independent from window exit, so we can't force that here,
260-
// but hopefully this alert will lead the user to do so themselves.
261-
}, 10);
262272
});
263273
}
264274

src/services/desktop-api.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { desktopVersion } from "./service-versions";
2+
13
type DesktopInjectedKey =
24
| 'httpToolkitDesktopVersion'
35
| 'httpToolkitForwardingDefault';
@@ -79,4 +81,24 @@ const global = typeof globalThis !== 'undefined'
7981
? window
8082
: {} as Window;
8183

82-
export const DesktopApi: DesktopApi = global.desktopApi ?? {};
84+
export const DesktopApi: DesktopApi = global.desktopApi ?? {};
85+
86+
export function canRestartApp(): boolean {
87+
return window.desktopApi?.restartApp !== undefined ||
88+
(desktopVersion.state === 'fulfilled' && !navigator.platform?.startsWith('Mac'));
89+
}
90+
91+
export function restartApp(): void {
92+
if (DesktopApi.restartApp) {
93+
// Where possible (recent desktop release) we restart the whole app directly
94+
DesktopApi.restartApp();
95+
} else if (desktopVersion.state === 'fulfilled' && !navigator.platform?.startsWith('Mac')) {
96+
// If not, on Windows & Linux desktop we just close the window to kill this process
97+
window.close();
98+
} else {
99+
// On Mac, app exit is independent from window exit, so on older desktop versions we can't
100+
// force a restart here, but hopefully this will lead the user to do so themselves:
101+
alert("Please fully quit the application to restart all internal components.");
102+
window.close();
103+
}
104+
}

0 commit comments

Comments
 (0)