Skip to content

Commit b59c05e

Browse files
committed
Detangle how we handle errors from the API
1 parent 4d70e38 commit b59c05e

File tree

10 files changed

+29
-78
lines changed

10 files changed

+29
-78
lines changed

ui/frontend/api.ts

Lines changed: 10 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fetch from 'isomorphic-fetch';
2+
import * as z from 'zod';
23

34
export const routes = {
45
compile: '/compile',
@@ -30,74 +31,24 @@ export function jsonPost(url: FetchArg, body: Record<string, any>): Promise<unkn
3031
});
3132
}
3233

34+
const ErrorResponse = z.object({
35+
error: z.string(),
36+
});
37+
type ErrorResponse = z.infer<typeof ErrorResponse>;
38+
3339
async function fetchJson(url: FetchArg, args: RequestInit) {
3440
const headers = new Headers(args.headers);
3541
headers.set('Content-Type', 'application/json');
3642

37-
let response;
38-
try {
39-
response = await fetch(url, { ...args, headers });
40-
} catch (networkError) {
41-
// e.g. server unreachable
42-
if (networkError instanceof Error) {
43-
throw {
44-
error: `Network error: ${networkError.toString()}`,
45-
};
46-
} else {
47-
throw {
48-
error: 'Unknown error while fetching JSON',
49-
};
50-
}
51-
}
52-
53-
let body;
54-
try {
55-
body = await response.json();
56-
} catch (convertError) {
57-
if (convertError instanceof Error) {
58-
throw {
59-
error: `Response was not JSON: ${convertError.toString()}`,
60-
};
61-
} else {
62-
throw {
63-
error: 'Unknown error while converting JSON',
64-
};
65-
}
66-
}
43+
const response = await fetch(url, { ...args, headers });
44+
const body = await response.json();
6745

6846
if (response.ok) {
6947
// HTTP 2xx
7048
return body;
7149
} else {
7250
// HTTP 4xx, 5xx (e.g. malformed JSON request)
73-
throw body;
51+
const error = await ErrorResponse.parseAsync(body);
52+
throw new Error(`The server reported an error: ${error.error}`);
7453
}
7554
}
76-
77-
// We made some strange decisions with how the `fetchJson` function
78-
// communicates errors, so we untwist those here to fit better with
79-
// redux-toolkit's ideas.
80-
export const adaptFetchError = async <R>(cb: () => Promise<R>): Promise<R> => {
81-
let result;
82-
83-
try {
84-
result = await cb();
85-
} catch (e) {
86-
if (e && typeof e === 'object' && 'error' in e && typeof e.error === 'string') {
87-
throw new Error(e.error);
88-
} else {
89-
throw new Error('An unknown error occurred');
90-
}
91-
}
92-
93-
if (
94-
result &&
95-
typeof result === 'object' &&
96-
'error' in result &&
97-
typeof result.error === 'string'
98-
) {
99-
throw new Error(result.error);
100-
}
101-
102-
return result;
103-
};

ui/frontend/compileActions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit';
22
import * as z from 'zod';
33

44
import { SimpleThunkAction } from './actions';
5-
import { adaptFetchError, jsonPost, routes } from './api';
5+
import { jsonPost, routes } from './api';
66
import { compileRequestPayloadSelector } from './selectors';
77

88
interface CompileRequestBody {
@@ -38,7 +38,7 @@ interface CompileActions {
3838

3939
export const makeCompileActions = ({ sliceName, target }: Props): CompileActions => {
4040
const action = createAsyncThunk(sliceName, async (payload: CompileRequestBody) => {
41-
const d = await adaptFetchError(() => jsonPost(routes.compile, payload));
41+
const d = await jsonPost(routes.compile, payload);
4242
return CompileResponseBody.parseAsync(d);
4343
});
4444

ui/frontend/reducers/crates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
22
import { sortBy } from 'lodash-es';
33
import * as z from 'zod';
44

5-
import { adaptFetchError, jsonGet, routes } from '../api';
5+
import { jsonGet, routes } from '../api';
66
import { Crate } from '../types';
77

88
const sliceName = 'crates';
@@ -17,7 +17,7 @@ const CratesResponse = z.object({
1717
type CratesResponse = z.infer<typeof CratesResponse>;
1818

1919
export const performCratesLoad = createAsyncThunk(sliceName, async () => {
20-
const d = await adaptFetchError(() => jsonGet(routes.meta.crates));
20+
const d = await jsonGet(routes.meta.crates);
2121
const crates = await CratesResponse.parseAsync(d);
2222
return sortBy(crates.crates, (c) => c.name);
2323
});

ui/frontend/reducers/output/clippy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
22
import * as z from 'zod';
33

4-
import { adaptFetchError, jsonPost, routes } from '../../api';
4+
import { jsonPost, routes } from '../../api';
55
import { clippyRequestSelector } from '../../selectors';
66
import RootState from '../../state';
77

@@ -38,7 +38,7 @@ export const performClippy = createAsyncThunk<ClippyResponseBody, void, { state:
3838
async (_arg: void, { getState }) => {
3939
const body: ClippyRequestBody = clippyRequestSelector(getState());
4040

41-
const d = await adaptFetchError(() => jsonPost(routes.clippy, body));
41+
const d = await jsonPost(routes.clippy, body);
4242
return ClippyResponseBody.parseAsync(d);
4343
},
4444
);

ui/frontend/reducers/output/execute.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AnyAction, Draft, createAsyncThunk, createSlice } from '@reduxjs/toolki
22
import * as z from 'zod';
33

44
import { SimpleThunkAction } from '../../actions';
5-
import { adaptFetchError, jsonPost, routes } from '../../api';
5+
import { jsonPost, routes } from '../../api';
66
import { executeRequestPayloadSelector, executeViaWebsocketSelector } from '../../selectors';
77
import { Channel, Edition, Mode } from '../../types';
88
import {
@@ -77,7 +77,7 @@ const ExecuteResponseBody = z.object({
7777
type ExecuteResponseBody = z.infer<typeof ExecuteResponseBody>;
7878

7979
export const performExecute = createAsyncThunk(sliceName, async (payload: ExecuteRequestBody) => {
80-
const d = await adaptFetchError(() => jsonPost(routes.execute, payload));
80+
const d = await jsonPost(routes.execute, payload);
8181
return ExecuteResponseBody.parseAsync(d);
8282
});
8383

ui/frontend/reducers/output/format.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
22
import * as z from 'zod';
33

4-
import { adaptFetchError, jsonPost, routes } from '../../api';
4+
import { jsonPost, routes } from '../../api';
55
import { formatRequestSelector } from '../../selectors';
66
import RootState from '../../state';
77

@@ -37,7 +37,7 @@ export const performFormat = createAsyncThunk<FormatResponseBody, void, { state:
3737
async (_arg: void, { getState }) => {
3838
const body: FormatRequestBody = formatRequestSelector(getState());
3939

40-
const d = await adaptFetchError(() => jsonPost(routes.format, body));
40+
const d = await jsonPost(routes.format, body);
4141
return FormatResponseBody.parseAsync(d);
4242
},
4343
);

ui/frontend/reducers/output/gist.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Draft, PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit';
22
import * as z from 'zod';
33

4-
import { adaptFetchError, jsonGet, jsonPost, routes } from '../../api';
4+
import { jsonGet, jsonPost, routes } from '../../api';
55
import { baseUrlSelector, codeSelector } from '../../selectors';
66
import RootState from '../../state';
77
import { Channel, Edition, Mode } from '../../types';
@@ -57,7 +57,7 @@ export const performGistLoad = createAsyncThunk<
5757
const gistUrl = new URL(routes.meta.gistLoad, baseUrl);
5858
const u = new URL(id, gistUrl);
5959

60-
const d = await adaptFetchError(() => jsonGet(u));
60+
const d = await jsonGet(u);
6161
const gist = await GistResponseBody.parseAsync(d);
6262
return { ...gist, channel, mode, edition, stdout: '', stderr: '' };
6363
});
@@ -74,7 +74,7 @@ export const performGistSave = createAsyncThunk<SuccessProps, void, { state: Roo
7474
},
7575
} = state;
7676

77-
const d = await adaptFetchError(() => jsonPost(routes.meta.gistSave, { code }));
77+
const d = await jsonPost(routes.meta.gistSave, { code });
7878
const gist = await GistResponseBody.parseAsync(d);
7979
return { ...gist, code, stdout, stderr, channel, mode, edition };
8080
},

ui/frontend/reducers/output/macroExpansion.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
22
import * as z from 'zod';
33

4-
import { adaptFetchError, jsonPost, routes } from '../../api';
4+
import { jsonPost, routes } from '../../api';
55
import { macroExpansionRequestSelector } from '../../selectors';
66
import RootState from '../../state';
77

@@ -38,7 +38,7 @@ export const performMacroExpansion = createAsyncThunk<
3838
>(sliceName, async (_arg: void, { getState }) => {
3939
const body: MacroExpansionRequestBody = macroExpansionRequestSelector(getState());
4040

41-
const d = await adaptFetchError(() => jsonPost(routes.macroExpansion, body));
41+
const d = await jsonPost(routes.macroExpansion, body);
4242
return MacroExpansionResponseBody.parseAsync(d);
4343
});
4444

ui/frontend/reducers/output/miri.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
22
import * as z from 'zod';
33

4-
import { adaptFetchError, jsonPost, routes } from '../../api';
4+
import { jsonPost, routes } from '../../api';
55
import { miriRequestSelector } from '../../selectors';
66
import RootState from '../../state';
77

@@ -36,7 +36,7 @@ export const performMiri = createAsyncThunk<MiriResponseBody, void, { state: Roo
3636
async (_arg: void, { getState }) => {
3737
const body: MiriRequestBody = miriRequestSelector(getState());
3838

39-
const d = await adaptFetchError(() => jsonPost(routes.miri, body));
39+
const d = await jsonPost(routes.miri, body);
4040
return MiriResponseBody.parseAsync(d);
4141
},
4242
);

ui/frontend/reducers/versions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
22
import * as z from 'zod';
33

4-
import { adaptFetchError, jsonGet, routes } from '../api';
4+
import { jsonGet, routes } from '../api';
55
import { ChannelVersion } from '../types';
66

77
const sliceName = 'versions';
@@ -19,7 +19,7 @@ const Response = z.object({
1919
type Response = z.infer<typeof Response>;
2020

2121
export const performVersionsLoad = createAsyncThunk(sliceName, async () => {
22-
const d = await adaptFetchError(() => jsonGet(routes.meta.versions));
22+
const d = await jsonGet(routes.meta.versions);
2323
return Response.parseAsync(d);
2424
});
2525

0 commit comments

Comments
 (0)