Skip to content

Commit e0e350c

Browse files
committed
Handle API errors on Send page
These generally come from bad input, but previously the UI just got stuck and didn't explain anything - now the request resets so you can try again, and an error alert appears.
1 parent 49479d1 commit e0e350c

File tree

4 files changed

+124
-105
lines changed

4 files changed

+124
-105
lines changed

src/components/send/send-page.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { styled } from '../../styles';
66
import { useHotkeys } from '../../util/ui';
77
import { WithInjected } from '../../types';
88

9+
import { ApiError } from '../../services/server-api-types';
910
import { SendStore } from '../../model/send/send-store';
1011

1112
import { ContainerSizedEditor } from '../editor/base-editor';
@@ -66,7 +67,13 @@ class SendPage extends React.Component<{
6667
selectedRequest
6768
} = this.props.sendStore;
6869

69-
sendRequest(selectedRequest);
70+
sendRequest(selectedRequest).catch(e => {
71+
console.log(e);
72+
const errorMessage = (e instanceof ApiError && e.apiErrorMessage)
73+
? e.apiErrorMessage
74+
: e.message ?? e;
75+
alert(errorMessage);
76+
});
7077
};
7178

7279
private showRequestOnViewPage = () => {

src/model/send/send-store.ts

+106-96
Original file line numberDiff line numberDiff line change
@@ -135,110 +135,120 @@ export class SendStore {
135135
const requestInput = sendRequest.request;
136136
const pendingRequestDeferred = getObservableDeferred();
137137
const abortController = new AbortController();
138-
runInAction(() => {
139-
sendRequest.sentExchange = undefined;
140138

141-
sendRequest.pendingSend = {
142-
promise: pendingRequestDeferred.promise,
143-
abort: () => abortController.abort()
144-
};
145-
146-
const clearPending = action(() => { sendRequest.pendingSend = undefined; });
147-
sendRequest.pendingSend.promise.then(clearPending, clearPending);
148-
});
139+
try {
140+
runInAction(() => {
141+
sendRequest.sentExchange = undefined;
149142

150-
const exchangeId = uuid();
143+
sendRequest.pendingSend = {
144+
promise: pendingRequestDeferred.promise,
145+
abort: () => abortController.abort()
146+
};
151147

152-
const passthroughOptions = this.rulesStore.activePassthroughOptions;
153-
154-
const url = new URL(requestInput.url);
155-
const effectivePort = getEffectivePort(url);
156-
const hostWithPort = `${url.hostname}:${effectivePort}`;
157-
const clientCertificate = passthroughOptions.clientCertificateHostMap?.[hostWithPort] ||
158-
passthroughOptions.clientCertificateHostMap?.[url.hostname!] ||
159-
undefined;
160-
161-
const requestOptions = {
162-
ignoreHostHttpsErrors: passthroughOptions.ignoreHostHttpsErrors,
163-
trustAdditionalCAs: this.rulesStore.additionalCaCertificates.map((cert) =>
164-
({ cert: cert.rawPEM })
165-
),
166-
clientCertificate,
167-
proxyConfig: getProxyConfig(this.rulesStore.proxyConfig),
168-
lookupOptions: passthroughOptions.lookupOptions
169-
};
148+
const clearPending = action(() => { sendRequest.pendingSend = undefined; });
149+
sendRequest.pendingSend.promise.then(clearPending, clearPending);
150+
});
170151

171-
const encodedBody = await requestInput.rawBody.encodingBestEffortPromise;
152+
const exchangeId = uuid();
153+
154+
const passthroughOptions = this.rulesStore.activePassthroughOptions;
155+
156+
const url = new URL(requestInput.url);
157+
const effectivePort = getEffectivePort(url);
158+
const hostWithPort = `${url.hostname}:${effectivePort}`;
159+
const clientCertificate = passthroughOptions.clientCertificateHostMap?.[hostWithPort] ||
160+
passthroughOptions.clientCertificateHostMap?.[url.hostname!] ||
161+
undefined;
162+
163+
const requestOptions = {
164+
ignoreHostHttpsErrors: passthroughOptions.ignoreHostHttpsErrors,
165+
trustAdditionalCAs: this.rulesStore.additionalCaCertificates.map((cert) =>
166+
({ cert: cert.rawPEM })
167+
),
168+
clientCertificate,
169+
proxyConfig: getProxyConfig(this.rulesStore.proxyConfig),
170+
lookupOptions: passthroughOptions.lookupOptions
171+
};
172172

173-
const responseStream = await ServerApi.sendRequest(
174-
{
175-
url: requestInput.url,
173+
const encodedBody = await requestInput.rawBody.encodingBestEffortPromise;
174+
175+
const responseStream = await ServerApi.sendRequest(
176+
{
177+
url: requestInput.url,
178+
method: requestInput.method,
179+
headers: requestInput.headers,
180+
rawBody: encodedBody
181+
},
182+
requestOptions,
183+
abortController.signal
184+
);
185+
186+
const exchange = this.eventStore.recordSentRequest({
187+
id: exchangeId,
188+
httpVersion: '1.1',
189+
matchedRuleId: false,
176190
method: requestInput.method,
177-
headers: requestInput.headers,
178-
rawBody: encodedBody
179-
},
180-
requestOptions,
181-
abortController.signal
182-
);
183-
184-
const exchange = this.eventStore.recordSentRequest({
185-
id: exchangeId,
186-
httpVersion: '1.1',
187-
matchedRuleId: false,
188-
method: requestInput.method,
189-
url: requestInput.url,
190-
protocol: url.protocol.slice(0, -1),
191-
path: url.pathname,
192-
hostname: url.hostname,
193-
headers: rawHeadersToHeaders(requestInput.headers),
194-
rawHeaders: _.cloneDeep(requestInput.headers),
195-
body: { buffer: encodedBody },
196-
timingEvents: {
197-
startTime: Date.now()
198-
} as TimingEvents,
199-
tags: ['httptoolkit:manually-sent-request']
200-
});
191+
url: requestInput.url,
192+
protocol: url.protocol.slice(0, -1),
193+
path: url.pathname,
194+
hostname: url.hostname,
195+
headers: rawHeadersToHeaders(requestInput.headers),
196+
rawHeaders: _.cloneDeep(requestInput.headers),
197+
body: { buffer: encodedBody },
198+
timingEvents: {
199+
startTime: Date.now()
200+
} as TimingEvents,
201+
tags: ['httptoolkit:manually-sent-request']
202+
});
201203

202-
// Keep the exchange up to date as response data arrives:
203-
trackResponseEvents(responseStream, exchange)
204-
.catch(action((error: ErrorLike & { timingEvents?: TimingEvents }) => {
205-
if (error.name === 'AbortError' && abortController.signal.aborted) {
206-
const startTime = exchange.timingEvents.startTime!; // Always set in Send case (just above)
207-
// Make a guess at an aborted timestamp, since this error won't give us one automatically:
208-
const durationBeforeAbort = Date.now() - startTime;
209-
const startTimestamp = exchange.timingEvents.startTimestamp ?? startTime;
210-
const abortedTimestamp = startTimestamp + durationBeforeAbort;
211-
212-
exchange.markAborted({
213-
id: exchange.id,
214-
error: {
215-
message: 'Request cancelled'
216-
},
217-
timingEvents: {
218-
startTimestamp,
219-
abortedTimestamp,
220-
...exchange.timingEvents,
221-
...error.timingEvents
222-
} as TimingEvents,
223-
tags: ['client-error:ECONNABORTED']
224-
});
225-
} else {
226-
exchange.markAborted({
227-
id: exchange.id,
228-
error: error,
229-
timingEvents: {
230-
...exchange.timingEvents as TimingEvents,
231-
...error.timingEvents
232-
},
233-
tags: error.code ? [`passthrough-error:${error.code}`] : []
234-
});
235-
}
236-
}))
237-
.then(() => pendingRequestDeferred.resolve());
204+
// Keep the exchange up to date as response data arrives:
205+
trackResponseEvents(responseStream, exchange)
206+
.catch(action((error: ErrorLike & { timingEvents?: TimingEvents }) => {
207+
if (error.name === 'AbortError' && abortController.signal.aborted) {
208+
const startTime = exchange.timingEvents.startTime!; // Always set in Send case (just above)
209+
// Make a guess at an aborted timestamp, since this error won't give us one automatically:
210+
const durationBeforeAbort = Date.now() - startTime;
211+
const startTimestamp = exchange.timingEvents.startTimestamp ?? startTime;
212+
const abortedTimestamp = startTimestamp + durationBeforeAbort;
213+
214+
exchange.markAborted({
215+
id: exchange.id,
216+
error: {
217+
message: 'Request cancelled'
218+
},
219+
timingEvents: {
220+
startTimestamp,
221+
abortedTimestamp,
222+
...exchange.timingEvents,
223+
...error.timingEvents
224+
} as TimingEvents,
225+
tags: ['client-error:ECONNABORTED']
226+
});
227+
} else {
228+
exchange.markAborted({
229+
id: exchange.id,
230+
error: error,
231+
timingEvents: {
232+
...exchange.timingEvents as TimingEvents,
233+
...error.timingEvents
234+
},
235+
tags: error.code ? [`passthrough-error:${error.code}`] : []
236+
});
237+
}
238+
}))
239+
.then(() => pendingRequestDeferred.resolve());
238240

239-
runInAction(() => {
240-
sendRequest.sentExchange = exchange;
241-
});
241+
runInAction(() => {
242+
sendRequest.sentExchange = exchange;
243+
});
244+
} catch (e: any) {
245+
pendingRequestDeferred.reject(e);
246+
runInAction(() => {
247+
sendRequest.pendingSend = undefined;
248+
sendRequest.sentExchange = undefined;
249+
});
250+
throw e;
251+
}
242252
}
243253

244254
}

src/services/server-api-types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export class ApiError extends Error {
2828
constructor(
2929
message: string,
3030
readonly operationName: string,
31-
readonly errorCode?: string | number
31+
readonly errorCode?: string | number,
32+
public apiErrorMessage?: string
3233
) {
3334
super(`API error during ${operationName}: ${message}`);
3435
}

src/services/server-rest-api.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export class RestApiClient {
5858
: undefined,
5959
signal: options?.abortSignal
6060
}).catch((e) => {
61-
throw new ApiError(`fetch failed with '${e.message ?? e}'`, operationName);
61+
const errorMessage = e.message ?? e;
62+
throw new ApiError(`fetch failed with '${errorMessage}'`, operationName);
6263
});
6364

6465
if (!response.ok) {
@@ -71,16 +72,16 @@ export class RestApiClient {
7172

7273
console.error(response.status, errorBody);
7374

75+
const errorMessage = errorBody?.error?.message ?? '[unknown]';
76+
const errorCode = errorBody?.error?.code;
77+
7478
throw new ApiError(
7579
`unexpected ${response.status} ${response.statusText} - ${
76-
errorBody?.error?.code
77-
? `${errorBody?.error?.code} -`
78-
: ''
79-
}${
80-
errorBody?.error?.message ?? '[unknown]'
80+
errorCode ? `${errorCode} -` : ''
8181
}`,
8282
operationName,
83-
response.status
83+
response.status,
84+
errorMessage
8485
);
8586
}
8687

0 commit comments

Comments
 (0)