Skip to content

Commit

Permalink
Expose request errors with dedicated error object (#99)
Browse files Browse the repository at this point in the history
* Added error for tracking invalid responses

* Ensure passing tests
  • Loading branch information
leo authored Jul 15, 2024
1 parent 071dd9e commit 1cee343
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 28 deletions.
Binary file modified bun.lockb
Binary file not shown.
10 changes: 2 additions & 8 deletions src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { processStorableObjects, uploadStorableObjects } from '@/src/storage';
import type { Query, Results } from '@/src/types/query';
import type { QueryHandlerOptions } from '@/src/types/utils';
import { runQueriesWithHooks } from '@/src/utils/data-hooks';
import { getDotNotatedPath, InvalidQueryError } from '@/src/utils/errors';
import { getDotNotatedPath, getResponseBody, InvalidQueryError } from '@/src/utils/errors';
import { formatTimeFields, getProperty } from '@/src/utils/helpers';

type QueryResponse<T> = {
Expand Down Expand Up @@ -85,14 +85,8 @@ export const runQueries = async <T>(

const fetcher = typeof options?.fetch === 'function' ? options.fetch : fetch;
const response = await fetcher(request);
const { results, error } = (await response.json()) as QueryResponse<T>;

// Throw errors that happened during the execution of the queries.
if (error) {
const exposedError: Error & { code?: string } = new Error(error.message);
if (error.code) exposedError.code = error.code;
throw exposedError;
}
const { results } = await getResponseBody<QueryResponse<T>>(response);

const startFormatting = performance.now();

Expand Down
12 changes: 6 additions & 6 deletions src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CombinedInstructions, Query } from '@/src/types/query';
import type { StorableObject, StoredObject } from '@/src/types/storage';
import type { QueryHandlerOptions } from '@/src/types/utils';
import { getResponseBody } from '@/src/utils/errors';

/**
* Extract `StorableObject`s from queries. These will be uploaded separately
Expand Down Expand Up @@ -98,14 +99,13 @@ export const uploadStorableObjects = async (
});

const response = await fetcher(request);
if (!response.ok) throw new Error(await response.text());
return response.json() as Promise<StoredObject>;
return getResponseBody<StoredObject>(response, {
errorPrefix:
'An error occurred while uploading the binary objects included in the provided queries. Error:',
});
});

return Promise.all(requests).catch((err) => {
const message = `An error occurred while uploading the binary objects included in the provided queries. ${err}`;
throw new Error(message);
});
return Promise.all(requests);
};

/**
Expand Down
62 changes: 56 additions & 6 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
interface Details {
interface InvalidQueryErrorDetails {
message: string;
query: string | null;
path: string | null;
details: string;
}

export class InvalidQueryError extends Error {
message: Details['message'];
query: Details['query'];
path: Details['path'];
details: Details['details'];
message: InvalidQueryErrorDetails['message'];
query: InvalidQueryErrorDetails['query'];
path: InvalidQueryErrorDetails['path'];
details: InvalidQueryErrorDetails['details'];

constructor(details: Details) {
constructor(details: InvalidQueryErrorDetails) {
super(details.message);

this.name = 'InvalidQueryError';
Expand All @@ -22,6 +22,56 @@ export class InvalidQueryError extends Error {
}
}

interface InvalidResponseErrorDetails {
message: string;
code: string;
}

export class InvalidResponseError extends Error {
message: InvalidResponseErrorDetails['message'];
code: InvalidResponseErrorDetails['code'];

constructor(details: InvalidResponseErrorDetails) {
super(details.message);

this.name = 'InvalidQueryRequest';
this.message = details.message;
this.code = details.code;
}
}

/**
* Parses the response as JSON or, alternatively, throws an error containing
* potential error details that might have been included in the response.
*
* @param response The response of a fetch request.
*
* @returns The response body as a JSON object.
*/
export const getResponseBody = async <T>(
response: Response,
options?: { errorPrefix?: string },
): Promise<T> => {
if (response.ok) return response.json() as T;

const text = await response.text();

let error: InvalidResponseErrorDetails = {
message: text,
code: 'UNKNOWN_ERROR',
};

try {
({ error } = JSON.parse(text));
} catch (err) {
// Ignore parsing errors
}

if (options?.errorPrefix) error.message = `${options.errorPrefix} ${error.message}`;

throw new InvalidResponseError(error);
};

/**
* Utility function to generate a dot-notated string of the given path.
*
Expand Down
11 changes: 9 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { runQueriesWithStorageAndHooks as runQueries } from '@/src/queries';
import { processStorableObjects } from '@/src/storage';
import { getBatchProxy, getSyntaxProxy } from '@/src/syntax/utils';
import { InvalidQueryError } from '@/src/utils/errors';
import { InvalidQueryError, InvalidResponseError } from '@/src/utils/errors';

export { runQueries, processStorableObjects, getSyntaxProxy, getBatchProxy, InvalidQueryError };
export {
runQueries,
processStorableObjects,
getSyntaxProxy,
getBatchProxy,
InvalidQueryError,
InvalidResponseError,
};
17 changes: 11 additions & 6 deletions tests/unit/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,18 @@ describe('queries handler', () => {
fetch: async (request) => {
mockRequestResolvedValue = request as Request;

return Response.json({
results: [],
error: {
message: 'Invalid query provided.',
code: 'BAD_REQUEST',
return new Response(
JSON.stringify({
results: [],
error: {
message: 'Invalid query provided.',
code: 'BAD_REQUEST',
},
}),
{
status: 400,
},
});
);
},
});

Expand Down

0 comments on commit 1cee343

Please sign in to comment.