diff --git a/.changeset/olive-rings-kick.md b/.changeset/olive-rings-kick.md
new file mode 100644
index 000000000..94407dd13
--- /dev/null
+++ b/.changeset/olive-rings-kick.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': patch
+---
+
+Do not throw nonserializable error in case of invalid URL
diff --git a/apps/website/docs/api/utils/error_creators.md b/apps/website/docs/api/utils/error_creators.md
index 70cbd8fcb..f8e446c8c 100644
--- a/apps/website/docs/api/utils/error_creators.md
+++ b/apps/website/docs/api/utils/error_creators.md
@@ -146,3 +146,26 @@ test('on error', async () => {
});
});
```
+
+## `configurationError`
+
+`ConfigurationError` is thrown when the query is misconfigured. E.g., when the URL is not URL.
+
+```ts
+import { configurationError } from '@farfetched/core';
+
+test('on error', async () => {
+ const scope = fork({
+ handlers: [
+ [
+ query.__.executeFx,
+ vi.fn(() => {
+ throw configurationError({
+ validationErrors: ['"LOL KEK" is not valid URL'],
+ });
+ }),
+ ],
+ ],
+ });
+});
+```
diff --git a/apps/website/docs/api/utils/error_guards.md b/apps/website/docs/api/utils/error_guards.md
index e38ac9d2c..157ef81cc 100644
--- a/apps/website/docs/api/utils/error_guards.md
+++ b/apps/website/docs/api/utils/error_guards.md
@@ -98,3 +98,16 @@ const networkProblems = sample({
filter: isNetworkError,
});
```
+
+## `inConfigurationError`
+
+`ConfigurationError` is thrown when the query is misconfigured. E.g., when the URL is not URL.
+
+```ts
+import { inConfigurationError } from '@farfetched/core';
+
+const configurationProblems = sample({
+ clock: query.finished.failure,
+ filter: inConfigurationError,
+});
+```
diff --git a/packages/core/index.ts b/packages/core/index.ts
index aca528559..0b90d5cce 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -79,6 +79,7 @@ export {
type PreparationError,
type HttpError,
type NetworkError,
+ type ConfigurationError,
} from './src/errors/type';
export {
invalidDataError,
@@ -87,6 +88,7 @@ export {
preparationError,
httpError,
networkError,
+ configurationError,
} from './src/errors/create_error';
export {
isTimeoutError,
diff --git a/packages/core/src/errors/create_error.ts b/packages/core/src/errors/create_error.ts
index 8e92738e6..4002967fc 100644
--- a/packages/core/src/errors/create_error.ts
+++ b/packages/core/src/errors/create_error.ts
@@ -13,6 +13,8 @@ import {
type PreparationError,
TIMEOUT,
type TimeoutError,
+ CONFIGURATION,
+ type ConfigurationError,
} from './type';
export function invalidDataError(config: {
@@ -74,3 +76,14 @@ export function networkError(config: {
explanation: 'Request was failed due to network problems',
};
}
+
+export function configurationError(config: {
+ reason: string;
+ validationErrors: string[];
+}): ConfigurationError {
+ return {
+ ...config,
+ errorType: CONFIGURATION,
+ explanation: 'Operation is misconfigured',
+ };
+}
diff --git a/packages/core/src/errors/guards.ts b/packages/core/src/errors/guards.ts
index fe0dca5b3..a0a2b4039 100644
--- a/packages/core/src/errors/guards.ts
+++ b/packages/core/src/errors/guards.ts
@@ -3,14 +3,16 @@ import {
type AbortError,
HTTP,
type HttpError,
- type InvalidDataError,
INVALID_DATA,
+ type InvalidDataError,
NETWORK,
- NetworkError,
+ type NetworkError,
PREPARATION,
- PreparationError,
+ type PreparationError,
TIMEOUT,
- TimeoutError,
+ type TimeoutError,
+ CONFIGURATION,
+ type ConfigurationError,
} from './type';
type WithError> = P & { error: T };
@@ -66,3 +68,9 @@ export function isNetworkError(
): args is WithError {
return args.error?.errorType === NETWORK;
}
+
+export function isConfigurationError(
+ args: WithError
+): args is WithError {
+ return args.error?.errorType === CONFIGURATION;
+}
diff --git a/packages/core/src/errors/type.ts b/packages/core/src/errors/type.ts
index 450c9ef5f..5867adfa9 100644
--- a/packages/core/src/errors/type.ts
+++ b/packages/core/src/errors/type.ts
@@ -39,3 +39,9 @@ export interface NetworkError extends FarfetchedError {
reason: string | null;
cause?: unknown;
}
+
+export const CONFIGURATION = 'CONFIGURATION';
+export interface ConfigurationError
+ extends FarfetchedError {
+ validationErrors: string[];
+}
diff --git a/packages/core/src/fetch/__tests__/api.request.url.test.ts b/packages/core/src/fetch/__tests__/api.request.url.test.ts
index 9dc5e54ed..6353adf05 100644
--- a/packages/core/src/fetch/__tests__/api.request.url.test.ts
+++ b/packages/core/src/fetch/__tests__/api.request.url.test.ts
@@ -1,6 +1,8 @@
-import { allSettled, createStore, fork } from 'effector';
+import { allSettled, createStore, fork, sample } from 'effector';
import { describe, test, expect, vi } from 'vitest';
+import { configurationError } from '../../errors/create_error';
+
import { createApiRequest } from '../api';
import { fetchFx } from '../fetch';
@@ -69,4 +71,26 @@ describe('fetch/api.request.url', () => {
await allSettled(callApiFx, { scope, params: {} });
expect(fetchMock.mock.calls[1][0].url).toEqual('https://new-api.salo.com/');
});
+
+ test('throw configuration error if url is invalid', async () => {
+ const callApiFx = createApiRequest({
+ request: { mapBody, credentials, url: 'LOL KEK', method },
+ response,
+ });
+
+ const $error = createStore(null);
+
+ sample({ clock: callApiFx.failData, target: $error });
+
+ const scope = fork();
+
+ await allSettled(callApiFx, { scope, params: {} });
+
+ expect(scope.getState($error)).toEqual(
+ configurationError({
+ reason: 'Invalid URL',
+ validationErrors: ['"LOL KEK" is not valid URL'],
+ })
+ );
+ });
});
diff --git a/packages/core/src/fetch/api.ts b/packages/core/src/fetch/api.ts
index 5b837a5d4..418ecfa08 100644
--- a/packages/core/src/fetch/api.ts
+++ b/packages/core/src/fetch/api.ts
@@ -16,6 +16,7 @@ import {
import { NonOptionalKeys } from '../libs/lohyphen';
import {
AbortError,
+ ConfigurationError,
HttpError,
InvalidDataError,
NetworkError,
@@ -143,6 +144,7 @@ interface ApiConfig, P>
}
export type ApiRequestError =
+ | ConfigurationError
| TimeoutError
| PreparationError
| NetworkError
diff --git a/packages/core/src/fetch/lib.ts b/packages/core/src/fetch/lib.ts
index e1a7e0dfb..a6d87d3f8 100644
--- a/packages/core/src/fetch/lib.ts
+++ b/packages/core/src/fetch/lib.ts
@@ -1,3 +1,5 @@
+import { configurationError } from '../errors/create_error';
+
export type FetchApiRecord = Record<
string,
string | string[] | number | boolean | null | undefined
@@ -70,7 +72,8 @@ export function formatHeaders(headersRecord: FetchApiRecord): Headers {
export function formatUrl(
url: string,
queryRecord: FetchApiRecord | string
-): string {
+): URL {
+ let urlString: string;
let queryString: string;
if (typeof queryRecord === 'string') {
@@ -80,10 +83,19 @@ export function formatUrl(
}
if (!queryString) {
- return url;
+ urlString = url;
+ } else {
+ urlString = `${url}?${queryString}`;
}
- return `${url}?${queryString}`;
+ try {
+ return new URL(urlString);
+ } catch (e) {
+ throw configurationError({
+ reason: 'Invalid URL',
+ validationErrors: [`"${urlString}" is not valid URL`],
+ });
+ }
}
function recordToUrlSearchParams(record: FetchApiRecord): URLSearchParams {