New fetchWithValidation / introducing simpleFetch

This commit is contained in:
Aleksandr Kraiz
2022-05-10 11:44:09 +04:00
parent edc91ecefe
commit 883bcf928f
16 changed files with 350 additions and 117 deletions

View File

@@ -1,55 +1,155 @@
import { Schema, z } from 'zod';
import fetch, { RequestInit } from 'node-fetch';
import { isWithError, isWithReason, HttpError } from './utils';
import fetch, { FetchError, RequestInit } from 'node-fetch';
import {
err, fromPromise, fromThrowable, ok,
} from 'neverthrow';
export class ExtendedError extends Error {
public url: string;
public status: number | null;
constructor(url: string, status: number | null, message: string) {
super();
this.url = url;
this.status = status;
this.message = message;
}
}
export const fetchJsonWithValidation = async <DataOut, DataIn, ErrorOut, ErrorIn>(
export default async function fetchWithValidation<DataOut, DataIn, ErrorOut, ErrorIn>(
url: string,
schema: Schema<DataOut, z.ZodTypeDef, DataIn>,
options?: RequestInit,
errorSchema?: Schema<ErrorOut, z.ZodTypeDef, ErrorIn>,
) => {
const response = await fetch(url, {
) {
// Cases:
// 1. fetchError (no network, connection refused, connection break)
// 2. unknownFetchError
// 3. unknownFetchThrow
// 4. unknownGetTextError
// 5. unknownGetTextUnknownError
// 6. serverError
// 7. jsonParseError
// 8. jsonParseUnknownError
// 9. clientErrorWithResponsePayload
// 10. clientErrorPayloadParseError
// 11. clientError
// 12. payloadParseError
// 13. payload
const fetchResult = await fromPromise(fetch(url, {
...options || {},
headers: {
'Cache-Control': 'no-store, max-age=0',
...(options ? options.headers : {}),
},
});
const text = await response.text();
// The ok read-only property of the Response interface contains a Boolean
// stating whether the response was successful (status in the range 200 - 299) or not.
if (!response.ok) {
throw new HttpError(response.status, text, 'HTTP', response.statusText);
}
const payload: unknown = JSON.parse(text);
try {
const data = schema.parse(payload);
return data;
} catch (e) {
if (errorSchema) {
const errorObj = errorSchema.parse(payload);
if (isWithError(errorObj) && isWithReason(errorObj.error)) {
throw new ExtendedError(url, response.status, errorObj.error.reason);
}
}), (e) => {
if (e instanceof FetchError) {
return err({
type: 'fetchError' as const,
url,
message: `${e.message} (${e.type})`,
error: e,
});
} if (e instanceof Error) {
return err({
type: 'unknownFetchError' as const,
url,
message: e.message,
error: e,
});
}
if (e instanceof Error) throw new ExtendedError(url, response.status, e.message);
throw e;
return err({
type: 'unknownFetchThrow' as const,
url,
message: 'Unknown fetch error',
error: e,
});
});
if (fetchResult.isErr()) return fetchResult.error;
const response = fetchResult.value;
const textResult = await fromPromise(response.text(), (e) => {
if (e instanceof Error) {
return err({
type: 'unknownGetTextError' as const,
url,
message: `Can't get response content: ${e.message}`,
error: e,
});
}
return err({
type: 'unknownGetTextUnknownError' as const,
url,
message: "Can't get response content: unknown error",
error: e,
});
});
if (textResult.isErr()) return textResult.error;
const text = textResult.value;
if (response.status >= 500) { // Server error
return err({
type: 'serverError' as const,
url,
message: `Server error: ${response.status} ${response.statusText}`,
status: response.status,
text,
});
}
};
const safeParseJson = fromThrowable(JSON.parse, (e) => {
if (e instanceof Error) {
return err({
type: 'jsonParseError' as const,
url,
message: e.message,
error: e,
});
}
return err({
type: 'jsonParseUnknownError' as const,
url,
message: 'Unknown JSON parse error',
error: e,
});
});
const jsonResult = safeParseJson(text);
if (jsonResult.isErr()) return jsonResult.error;
const json: unknown = jsonResult.value;
if (response.status >= 400) { // Client error
if (errorSchema) {
const serverError = errorSchema.safeParse(json);
if (serverError.success) {
return err({
type: 'clientErrorWithResponsePayload' as const,
url,
message: `Client error: ${response.status} ${response.statusText}`,
status: response.status,
payload: serverError.data,
});
}
return err({
type: 'clientErrorPayloadParseError' as const,
message: 'Can\'t recognize error message',
status: response.status,
text,
error: serverError.error,
});
}
return err({
type: 'clientError' as const,
url,
message: `Error: ${response.status} ${response.statusText}`,
status: response.status,
text,
});
}
const payload = schema.safeParse(json);
if (!payload.success) {
return err({
type: 'payloadParseError' as const,
url,
message: 'Can\'t recognize response payload',
error: payload.error,
});
}
return ok(payload.data);
}