Skip to content

Commit

Permalink
Do not throw nonserializable error in case of invalid URL
Browse files Browse the repository at this point in the history
  • Loading branch information
igorkamyshev committed Nov 27, 2023
1 parent c925412 commit ec2ab95
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-rings-kick.md
@@ -0,0 +1,5 @@
---
'@farfetched/core': patch
---

Do not throw nonserializable error in case of invalid URL
23 changes: 23 additions & 0 deletions apps/website/docs/api/utils/error_creators.md
Expand Up @@ -146,3 +146,26 @@ test('on error', async () => {
});
});
```

## `configurationError` <Badge type="tip" text="since v0.11.0" />

`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'],
});
}),
],
],
});
});
```
13 changes: 13 additions & 0 deletions apps/website/docs/api/utils/error_guards.md
Expand Up @@ -98,3 +98,16 @@ const networkProblems = sample({
filter: isNetworkError,
});
```

## `inConfigurationError` <Badge type="tip" text="since v0.11.0" />

`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,
});
```
2 changes: 2 additions & 0 deletions packages/core/index.ts
Expand Up @@ -79,6 +79,7 @@ export {
type PreparationError,
type HttpError,
type NetworkError,
type ConfigurationError,
} from './src/errors/type';
export {
invalidDataError,
Expand All @@ -87,6 +88,7 @@ export {
preparationError,
httpError,
networkError,
configurationError,
} from './src/errors/create_error';
export {
isTimeoutError,
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/errors/create_error.ts
Expand Up @@ -13,6 +13,8 @@ import {
type PreparationError,
TIMEOUT,
type TimeoutError,
CONFIGURATION,
type ConfigurationError,
} from './type';

export function invalidDataError(config: {
Expand Down Expand Up @@ -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',
};
}
16 changes: 12 additions & 4 deletions packages/core/src/errors/guards.ts
Expand Up @@ -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<T = any, P = Record<string, unknown>> = P & { error: T };
Expand Down Expand Up @@ -66,3 +68,9 @@ export function isNetworkError(
): args is WithError<NetworkError> {
return args.error?.errorType === NETWORK;
}

export function isConfigurationError(
args: WithError
): args is WithError<ConfigurationError> {
return args.error?.errorType === CONFIGURATION;
}
6 changes: 6 additions & 0 deletions packages/core/src/errors/type.ts
Expand Up @@ -39,3 +39,9 @@ export interface NetworkError extends FarfetchedError<typeof NETWORK> {
reason: string | null;
cause?: unknown;
}

export const CONFIGURATION = 'CONFIGURATION';
export interface ConfigurationError
extends FarfetchedError<typeof CONFIGURATION> {
validationErrors: string[];
}
26 changes: 25 additions & 1 deletion 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';

Expand Down Expand Up @@ -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<any>(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'],
})
);
});
});
2 changes: 2 additions & 0 deletions packages/core/src/fetch/api.ts
Expand Up @@ -16,6 +16,7 @@ import {
import { NonOptionalKeys } from '../libs/lohyphen';
import {
AbortError,
ConfigurationError,
HttpError,
InvalidDataError,
NetworkError,
Expand Down Expand Up @@ -143,6 +144,7 @@ interface ApiConfig<B, R extends CreationRequestConfig<B>, P>
}

export type ApiRequestError =
| ConfigurationError
| TimeoutError
| PreparationError
| NetworkError
Expand Down
18 changes: 15 additions & 3 deletions 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
Expand Down Expand Up @@ -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') {
Expand All @@ -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 {
Expand Down

0 comments on commit ec2ab95

Please sign in to comment.