Skip to content

Commit 94871da

Browse files
authored
Raise ParseError if non-200 response returns invalid JSON (#1320)
## Description Missed a codepath in #1313 where non-200 responses would attempt to parse the body
1 parent bfd7dc5 commit 94871da

File tree

4 files changed

+120
-4
lines changed

4 files changed

+120
-4
lines changed

src/common/exceptions/parse-error.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,23 @@ export class ParseError extends Error implements RequestException {
44
readonly name = 'ParseError';
55
readonly status = 500;
66
readonly rawBody: string;
7+
readonly rawStatus: number;
78
readonly requestID: string;
89

9-
constructor(message: string, rawBody: string, requestID: string) {
10+
constructor({
11+
message,
12+
rawBody,
13+
rawStatus,
14+
requestID,
15+
}: {
16+
message: string;
17+
rawBody: string;
18+
requestID: string;
19+
rawStatus: number;
20+
}) {
1021
super(message);
1122
this.rawBody = rawBody;
23+
this.rawStatus = rawStatus;
1224
this.requestID = requestID;
1325
}
1426
}

src/common/net/fetch-client.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fetch from 'jest-fetch-mock';
22
import { fetchOnce, fetchURL } from '../../common/utils/test-utils';
33
import { FetchHttpClient } from './fetch-client';
4+
import { ParseError } from '../exceptions/parse-error';
45

56
const fetchClient = new FetchHttpClient('https://test.workos.com', {
67
headers: {
@@ -224,4 +225,82 @@ describe('Fetch client', () => {
224225
expect(await response.toJSON()).toEqual({ data: 'response' });
225226
});
226227
});
228+
229+
describe('error handling', () => {
230+
it('should throw ParseError when response body is not valid JSON on non-200 status', async () => {
231+
// Mock a 500 response with invalid JSON (like an HTML error page)
232+
fetch.mockResponseOnce(
233+
'<html><body>Internal Server Error</body></html>',
234+
{
235+
status: 500,
236+
statusText: 'Internal Server Error',
237+
headers: {
238+
'X-Request-ID': 'test-request-123',
239+
'Content-Type': 'text/html',
240+
},
241+
},
242+
);
243+
244+
await expect(fetchClient.get('/users', {})).rejects.toThrow(ParseError);
245+
246+
try {
247+
await fetchClient.get('/users', {});
248+
} catch (error) {
249+
expect(error).toBeInstanceOf(ParseError);
250+
const parseError = error as ParseError;
251+
expect(parseError.message).toContain('Unexpected token');
252+
expect(parseError.rawBody).toBe(
253+
'<html><body>Internal Server Error</body></html>',
254+
);
255+
expect(parseError.requestID).toBe('test-request-123');
256+
expect(parseError.rawStatus).toBe(500);
257+
}
258+
});
259+
260+
it('should throw ParseError for non-FGA endpoints with invalid JSON response', async () => {
261+
// Test with a non-FGA endpoint to ensure the error handling works for regular requests too
262+
fetch.mockResponseOnce('Not JSON content', {
263+
status: 400,
264+
statusText: 'Bad Request',
265+
headers: {
266+
'X-Request-ID': 'bad-request-456',
267+
'Content-Type': 'text/plain',
268+
},
269+
});
270+
271+
await expect(
272+
fetchClient.post('/organizations', { name: 'Test' }, {}),
273+
).rejects.toThrow(ParseError);
274+
275+
try {
276+
await fetchClient.post('/organizations', { name: 'Test' }, {});
277+
} catch (error) {
278+
expect(error).toBeInstanceOf(ParseError);
279+
const parseError = error as ParseError;
280+
expect(parseError.rawBody).toBe('Not JSON content');
281+
expect(parseError.requestID).toBe('bad-request-456');
282+
expect(parseError.rawStatus).toBe(400);
283+
}
284+
});
285+
286+
it('should throw ParseError when X-Request-ID header is missing', async () => {
287+
fetch.mockResponseOnce('Invalid JSON Response', {
288+
status: 422,
289+
statusText: 'Unprocessable Entity',
290+
headers: {
291+
'Content-Type': 'application/json',
292+
},
293+
});
294+
295+
try {
296+
await fetchClient.put('/users/123', { name: 'Updated' }, {});
297+
} catch (error) {
298+
expect(error).toBeInstanceOf(ParseError);
299+
const parseError = error as ParseError;
300+
expect(parseError.rawBody).toBe('Invalid JSON Response');
301+
expect(parseError.requestID).toBe(''); // Should default to empty string when header is missing
302+
expect(parseError.rawStatus).toBe(422);
303+
}
304+
});
305+
});
227306
});

src/common/net/fetch-client.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ResponseHeaders,
77
} from '../interfaces/http-client.interface';
88
import { HttpClient, HttpClientError, HttpClientResponse } from './http-client';
9+
import { ParseError } from '../exceptions/parse-error';
910

1011
export class FetchHttpClient extends HttpClient implements HttpClientInterface {
1112
private readonly _fetchFn;
@@ -181,16 +182,34 @@ export class FetchHttpClient extends HttpClient implements HttpClientInterface {
181182
});
182183

183184
if (!res.ok) {
185+
const requestID = res.headers.get('X-Request-ID') ?? '';
186+
const rawBody = await res.text();
187+
188+
let responseJson: any;
189+
190+
try {
191+
responseJson = JSON.parse(rawBody);
192+
} catch (error) {
193+
if (error instanceof SyntaxError) {
194+
throw new ParseError({
195+
message: error.message,
196+
rawBody,
197+
requestID,
198+
rawStatus: res.status,
199+
});
200+
}
201+
throw error;
202+
}
203+
184204
throw new HttpClientError({
185205
message: res.statusText,
186206
response: {
187207
status: res.status,
188208
headers: res.headers,
189-
data: await res.json(),
209+
data: responseJson,
190210
},
191211
});
192212
}
193-
194213
return new FetchHttpClientResponse(res);
195214
}
196215

src/workos.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,14 @@ export class WorkOS {
278278
if (error instanceof SyntaxError) {
279279
const rawResponse = res.getRawResponse() as Response;
280280
const requestID = rawResponse.headers.get('X-Request-ID') ?? '';
281+
const rawStatus = rawResponse.status;
281282
const rawBody = await rawResponse.text();
282-
throw new ParseError(error.message, rawBody, requestID);
283+
throw new ParseError({
284+
message: error.message,
285+
rawBody,
286+
rawStatus,
287+
requestID,
288+
});
283289
}
284290
}
285291

0 commit comments

Comments
 (0)