From 1cee3430b8bbb89da5d7a5196a89aa468eb3f9db Mon Sep 17 00:00:00 2001 From: Leo Lamprecht Date: Mon, 15 Jul 2024 16:46:26 +0200 Subject: [PATCH] Expose request errors with dedicated error object (#99) * Added error for tracking invalid responses * Ensure passing tests --- bun.lockb | Bin 167528 -> 167536 bytes src/queries.ts | 10 ++---- src/storage.ts | 12 +++---- src/utils/errors.ts | 62 +++++++++++++++++++++++++++++++++---- src/utils/index.ts | 11 +++++-- tests/unit/queries.test.ts | 17 ++++++---- 6 files changed, 84 insertions(+), 28 deletions(-) diff --git a/bun.lockb b/bun.lockb index fa3684269f388e7296293a006ec07df7acb4b060..3c8e9aa58880da6d891c780aa1b9c0025b54676c 100755 GIT binary patch delta 914 zcmYk3Pe@cz6vpozbM%d+n3!5rj-pVCh~uWL$gM;ot*TW*feRf$1aVOfYNV+cXtzHT z^k-})Oe@jxRypx-Kui$%b0vllTTRsf%XxXq+n;)ofQ4gsBn7>~esI2Vo*AReo)2)+v9 z)=!*$gAq?vDh8T{XFL--4BsY}6e|I15_|4CIN}9To>&U~gShA`wI6}sf*7yGcwbX{ z#F&(O$q};}Q2{)tQd;dtk)IOFsP7o^T8J+>=l*iUyxNZgU4&=66FUKa7M{y5;K^-y zUoB$3YfWRsd$mVVNk delta 884 zcmXw#J4hU16ouzs@iDtTSS3CpDk=t|R;JP*xUEPe*u^R-thG`s0>Yw^L|sAo6I_j= z?&>Z&8y}ek6YZ?Wm4CYVn*nb?ctM)pe9>kO1)%EN={o`pcgA6LPahxqQej9+}}nt_}IcKg_#6H&c|$GyZ5%AY$Ab NH = { @@ -85,14 +85,8 @@ export const runQueries = async ( const fetcher = typeof options?.fetch === 'function' ? options.fetch : fetch; const response = await fetcher(request); - const { results, error } = (await response.json()) as QueryResponse; - // 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>(response); const startFormatting = performance.now(); diff --git a/src/storage.ts b/src/storage.ts index 3f1bce0..010be59 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -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 @@ -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; + return getResponseBody(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); }; /** diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 31b504d..5d63737 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,4 +1,4 @@ -interface Details { +interface InvalidQueryErrorDetails { message: string; query: string | null; path: string | null; @@ -6,12 +6,12 @@ interface Details { } 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'; @@ -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 ( + response: Response, + options?: { errorPrefix?: string }, +): Promise => { + 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. * diff --git a/src/utils/index.ts b/src/utils/index.ts index 02b09d9..5360685 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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, +}; diff --git a/tests/unit/queries.test.ts b/tests/unit/queries.test.ts index ccfa025..02a04d1 100644 --- a/tests/unit/queries.test.ts +++ b/tests/unit/queries.test.ts @@ -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, }, - }); + ); }, });