[FIX] Correct the fail retur API to transmit the error from backend when compatible and wrat it when error occured

This commit is contained in:
Edouard DUPIN 2024-05-31 19:52:00 +02:00
parent 91849094cd
commit 1fe3cc3523

View File

@ -4,389 +4,442 @@
* @license MPL-2 * @license MPL-2
*/ */
import { RestErrorResponse } from "./model"; import { RestErrorResponse, isRestErrorResponse } from "./model";
export enum HTTPRequestModel { export enum HTTPRequestModel {
DELETE = "DELETE", DELETE = "DELETE",
GET = "GET", GET = "GET",
PATCH = "PATCH", PATCH = "PATCH",
POST = "POST", POST = "POST",
PUT = "PUT", PUT = "PUT",
} }
export enum HTTPMimeType { export enum HTTPMimeType {
ALL = "*/*", ALL = "*/*",
CSV = "text/csv", CSV = "text/csv",
IMAGE = "image/*", IMAGE = "image/*",
IMAGE_JPEG = "image/jpeg", IMAGE_JPEG = "image/jpeg",
IMAGE_PNG = "image/png", IMAGE_PNG = "image/png",
JSON = "application/json", JSON = "application/json",
MULTIPART = "multipart/form-data", MULTIPART = "multipart/form-data",
OCTET_STREAM = "application/octet-stream", OCTET_STREAM = "application/octet-stream",
TEXT_PLAIN = "text/plain", TEXT_PLAIN = "text/plain",
} }
export interface RESTConfig { export interface RESTConfig {
// base of the server: http(s)://my.server.org/plop/api/ // base of the server: http(s)://my.server.org/plop/api/
server: string; server: string;
// Token to access of the data. // Token to access of the data.
token?: string; token?: string;
} }
export interface RESTModel { export interface RESTModel {
// base of the local API request: "sheep/{id}". // base of the local API request: "sheep/{id}".
endPoint: string; endPoint: string;
// Type of the request. // Type of the request.
requestType?: HTTPRequestModel; requestType?: HTTPRequestModel;
// Input type requested. // Input type requested.
accept?: HTTPMimeType; accept?: HTTPMimeType;
// Content of the local data. // Content of the local data.
contentType?: HTTPMimeType; contentType?: HTTPMimeType;
// Mode of the TOKEN in URL or Header (?token:${tokenInUrl}) // Mode of the TOKEN in URL or Header (?token:${tokenInUrl})
tokenInUrl?: boolean; tokenInUrl?: boolean;
} }
export interface ModelResponseHttp { export interface ModelResponseHttp {
status: number; status: number;
data: any; data: any;
} }
function isNullOrUndefined(data: any): data is undefined | null { function isNullOrUndefined(data: any): data is undefined | null {
return data === undefined || data === null; return data === undefined || data === null;
} }
// generic progression callback // generic progression callback
export type ProgressCallback = (count: number, total: number) => void; export type ProgressCallback = (count: number, total: number) => void;
export interface RESTAbort { export interface RESTAbort {
abort?: () => boolean; abort?: () => boolean;
} }
// Rest generic callback have a basic model to upload and download advancement. // Rest generic callback have a basic model to upload and download advancement.
export interface RESTCallbacks { export interface RESTCallbacks {
progressUpload?: ProgressCallback; progressUpload?: ProgressCallback;
progressDownload?: ProgressCallback; progressDownload?: ProgressCallback;
abortHandle?: RESTAbort; abortHandle?: RESTAbort;
} }
export interface RESTRequestType { export interface RESTRequestType {
restModel: RESTModel; restModel: RESTModel;
restConfig: RESTConfig; restConfig: RESTConfig;
data?: any; data?: any;
params?: object; params?: object;
queries?: object; queries?: object;
callback?: RESTCallbacks; callback?: RESTCallbacks;
} }
function replaceAll(input, searchValue, replaceValue) { function replaceAll(input, searchValue, replaceValue) {
return input.split(searchValue).join(replaceValue); return input.split(searchValue).join(replaceValue);
} }
function removeTrailingSlashes(input: string): string { function removeTrailingSlashes(input: string): string {
if (isNullOrUndefined(input)) { if (isNullOrUndefined(input)) {
return "undefined"; return "undefined";
} }
return input.replace(/\/+$/, ""); return input.replace(/\/+$/, "");
} }
function removeLeadingSlashes(input: string): string { function removeLeadingSlashes(input: string): string {
if (isNullOrUndefined(input)) { if (isNullOrUndefined(input)) {
return ""; return "";
} }
return input.replace(/^\/+/, ""); return input.replace(/^\/+/, "");
} }
export function RESTUrl({ export function RESTUrl({
restModel, restModel,
restConfig, restConfig,
params, params,
queries, queries,
}: RESTRequestType): string { }: RESTRequestType): string {
// Create the URL PATH: // Create the URL PATH:
let generateUrl = `${removeTrailingSlashes( let generateUrl = `${removeTrailingSlashes(
restConfig.server restConfig.server
)}/${removeLeadingSlashes(restModel.endPoint)}`; )}/${removeLeadingSlashes(restModel.endPoint)}`;
if (params !== undefined) { if (params !== undefined) {
for (let key of Object.keys(params)) { for (let key of Object.keys(params)) {
generateUrl = replaceAll(generateUrl, `{${key}}`, `${params[key]}`); generateUrl = replaceAll(generateUrl, `{${key}}`, `${params[key]}`);
}
}
if (
queries === undefined &&
(restConfig.token === undefined || restModel.tokenInUrl !== true)
) {
return generateUrl;
}
const searchParams = new URLSearchParams();
if (queries !== undefined) {
for (let key of Object.keys(queries)) {
const value = queries[key];
if (Array.isArray(value)) {
for (const element of value) {
searchParams.append(`${key}`, `${element}`);
} }
} else {
searchParams.append(`${key}`, `${value}`);
}
} }
if ( }
queries === undefined && if (restConfig.token !== undefined && restModel.tokenInUrl === true) {
(restConfig.token === undefined || restModel.tokenInUrl !== true) searchParams.append("Authorization", `Bearer ${restConfig.token}`);
) { }
return generateUrl; return generateUrl + "?" + searchParams.toString();
}
const searchParams = new URLSearchParams();
if (queries !== undefined) {
for (let key of Object.keys(queries)) {
const value = queries[key];
if (Array.isArray(value)) {
for (const element of value) {
searchParams.append(`${key}`, `${element}`);
}
} else {
searchParams.append(`${key}`, `${value}`);
}
}
}
if (restConfig.token !== undefined && restModel.tokenInUrl === true) {
searchParams.append("Authorization", `Bearer ${restConfig.token}`);
}
return generateUrl + "?" + searchParams.toString();
} }
export function fetchProgress( export function fetchProgress(
generateUrl: string, generateUrl: string,
{ {
method, method,
headers, headers,
body, body,
}: { }: {
method: HTTPRequestModel; method: HTTPRequestModel;
headers: any; headers: any;
body: any; body: any;
}, },
{ progressUpload, progressDownload, abortHandle }: RESTCallbacks { progressUpload, progressDownload, abortHandle }: RESTCallbacks
): Promise<Response> { ): Promise<Response> {
const xhr: { const xhr: {
io?: XMLHttpRequest; io?: XMLHttpRequest;
} = { } = {
io: new XMLHttpRequest(), io: new XMLHttpRequest(),
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Stream the upload progress // Stream the upload progress
if (progressUpload) { if (progressUpload) {
xhr.io?.upload.addEventListener("progress", (dataEvent) => { xhr.io?.upload.addEventListener("progress", (dataEvent) => {
if (dataEvent.lengthComputable) { if (dataEvent.lengthComputable) {
progressUpload(dataEvent.loaded, dataEvent.total); progressUpload(dataEvent.loaded, dataEvent.total);
}
});
} }
// Stream the download progress });
if (progressDownload) { }
xhr.io?.addEventListener("progress", (dataEvent) => { // Stream the download progress
if (dataEvent.lengthComputable) { if (progressDownload) {
progressDownload(dataEvent.loaded, dataEvent.total); xhr.io?.addEventListener("progress", (dataEvent) => {
} if (dataEvent.lengthComputable) {
}); progressDownload(dataEvent.loaded, dataEvent.total);
} }
if (abortHandle) { });
abortHandle.abort = () => { }
if (xhr.io) { if (abortHandle) {
console.log(`Request abort on the XMLHttpRequest: ${generateUrl}`); abortHandle.abort = () => {
xhr.io.abort(); if (xhr.io) {
return true; console.log(`Request abort on the XMLHttpRequest: ${generateUrl}`);
} xhr.io.abort();
console.log( return true;
`Request abort (FAIL) on the XMLHttpRequest: ${generateUrl}`
);
return false;
};
} }
// Check if we have an internal Fail: console.log(
xhr.io?.addEventListener("error", () => { `Request abort (FAIL) on the XMLHttpRequest: ${generateUrl}`
xhr.io = undefined; );
reject(new TypeError("Failed to fetch")); return false;
}); };
}
// Capture the end of the stream // Check if we have an internal Fail:
xhr.io?.addEventListener("loadend", () => { xhr.io?.addEventListener("error", () => {
if (xhr.io?.readyState !== XMLHttpRequest.DONE) { xhr.io = undefined;
return; reject(new TypeError("Failed to fetch"));
}
if (xhr.io?.status === 0) {
//the stream has been aborted
reject(new TypeError("Fetch has been aborted"));
return;
}
// Stream is ended, transform in a generic response:
const response = new Response(xhr.io.response, {
status: xhr.io.status,
statusText: xhr.io.statusText,
});
const headersArray = replaceAll(
xhr.io.getAllResponseHeaders().trim(),
"\r\n",
"\n"
).split("\n");
headersArray.forEach(function (header) {
const firstColonIndex = header.indexOf(":");
if (firstColonIndex !== -1) {
const key = header.substring(0, firstColonIndex).trim();
const value = header.substring(firstColonIndex + 1).trim();
response.headers.set(key, value);
} else {
response.headers.set(header, "");
}
});
xhr.io = undefined;
resolve(response);
});
xhr.io?.open(method, generateUrl, true);
if (!isNullOrUndefined(headers)) {
for (const [key, value] of Object.entries(headers)) {
xhr.io?.setRequestHeader(key, value as string);
}
}
xhr.io?.send(body);
}); });
// Capture the end of the stream
xhr.io?.addEventListener("loadend", () => {
if (xhr.io?.readyState !== XMLHttpRequest.DONE) {
return;
}
if (xhr.io?.status === 0) {
//the stream has been aborted
reject(new TypeError("Fetch has been aborted"));
return;
}
// Stream is ended, transform in a generic response:
const response = new Response(xhr.io.response, {
status: xhr.io.status,
statusText: xhr.io.statusText,
});
const headersArray = replaceAll(
xhr.io.getAllResponseHeaders().trim(),
"\r\n",
"\n"
).split("\n");
headersArray.forEach(function (header) {
const firstColonIndex = header.indexOf(":");
if (firstColonIndex !== -1) {
const key = header.substring(0, firstColonIndex).trim();
const value = header.substring(firstColonIndex + 1).trim();
response.headers.set(key, value);
} else {
response.headers.set(header, "");
}
});
xhr.io = undefined;
resolve(response);
});
xhr.io?.open(method, generateUrl, true);
if (!isNullOrUndefined(headers)) {
for (const [key, value] of Object.entries(headers)) {
xhr.io?.setRequestHeader(key, value as string);
}
}
xhr.io?.send(body);
});
} }
export function RESTRequest({ export function RESTRequest({
restModel, restModel,
restConfig, restConfig,
data, data,
params, params,
queries, queries,
callback, callback,
}: RESTRequestType): Promise<ModelResponseHttp> { }: RESTRequestType): Promise<ModelResponseHttp> {
// Create the URL PATH: // Create the URL PATH:
let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries }); let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries });
let headers: any = {}; let headers: any = {};
if (restConfig.token !== undefined && restModel.tokenInUrl !== true) { if (restConfig.token !== undefined && restModel.tokenInUrl !== true) {
headers["Authorization"] = `Bearer ${restConfig.token}`; headers["Authorization"] = `Bearer ${restConfig.token}`;
}
if (restModel.accept !== undefined) {
headers["Accept"] = restModel.accept;
}
if (restModel.requestType !== HTTPRequestModel.GET) {
// if Get we have not a content type, the body is empty
if (restModel.contentType !== HTTPMimeType.MULTIPART) {
// special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****"
headers["Content-Type"] = restModel.contentType;
} }
if (restModel.accept !== undefined) { }
headers["Accept"] = restModel.accept; let body = data;
if (restModel.contentType === HTTPMimeType.JSON) {
body = JSON.stringify(data);
} else if (restModel.contentType === HTTPMimeType.MULTIPART) {
const formData = new FormData();
for (const name in data) {
formData.append(name, data[name]);
} }
if (restModel.requestType !== HTTPRequestModel.GET) { body = formData;
// if Get we have not a content type, the body is empty }
if (restModel.contentType !== HTTPMimeType.MULTIPART) { return new Promise((resolve, reject) => {
// special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****" let action: undefined | Promise<Response> = undefined;
headers["Content-Type"] = restModel.contentType; if (
} isNullOrUndefined(callback) ||
(isNullOrUndefined(callback.progressDownload) &&
isNullOrUndefined(callback.progressUpload) &&
isNullOrUndefined(callback.abortHandle))
) {
// No information needed: call the generic fetch interface
action = fetch(generateUrl, {
method: restModel.requestType,
headers,
body,
});
} else {
// need progression information: call old fetch model (XMLHttpRequest) that permit to keep % upload and % download for HTTP1.x
action = fetchProgress(
generateUrl,
{
method: restModel.requestType ?? HTTPRequestModel.GET,
headers,
body,
},
callback
);
} }
let body = data; action
if (restModel.contentType === HTTPMimeType.JSON) { .then((response: Response) => {
body = JSON.stringify(data); if (response.status >= 200 && response.status <= 299) {
} else if (restModel.contentType === HTTPMimeType.MULTIPART) { const contentType = response.headers.get("Content-Type");
const formData = new FormData(); if (
for (const name in data) { !isNullOrUndefined(restModel.accept) &&
formData.append(name, data[name]); restModel.accept !== contentType
} ) {
body = formData; reject({
} name: "Model accept type incompatible",
return new Promise((resolve, reject) => { time: Date().toString(),
let action: undefined | Promise<Response> = undefined; status: 901,
if ( message: `REST Content type are not compatible: ${restModel.accept} != ${contentType}`,
isNullOrUndefined(callback) || statusMessage: "Fetch error",
(isNullOrUndefined(callback.progressDownload) && error: "rest-tools.ts Wrong type in the message return type",
isNullOrUndefined(callback.progressUpload) && } as RestErrorResponse);
isNullOrUndefined(callback.abortHandle)) } else if (contentType === HTTPMimeType.JSON) {
) { response
// No information needed: call the generic fetch interface .json()
action = fetch(generateUrl, { .then((value: any) => {
method: restModel.requestType, resolve({ status: response.status, data: value });
headers, })
body, .catch((reason: Error) => {
});
} else {
// need progression information: call old fetch model (XMLHttpRequest) that permit to keep % upload and % download for HTTP1.x
action = fetchProgress(
generateUrl,
{
method: restModel.requestType ?? HTTPRequestModel.GET,
headers,
body,
},
callback
);
}
action
.then((response: Response) => {
if (response.status >= 200 && response.status <= 299) {
const contentType = response.headers.get("Content-Type");
if (
!isNullOrUndefined(restModel.accept) &&
restModel.accept !== contentType
) {
reject({
name: "Model accept type incompatible",
time: Date().toString(),
status: 901,
error: `REST check wrong type: ${restModel.accept} != ${contentType}`,
statusMessage: "Fetch error",
message: "rest-tools.ts Wrong type in the message return type",
} as RestErrorResponse);
} else if (contentType === HTTPMimeType.JSON) {
response
.json()
.then((value: any) => {
resolve({ status: response.status, data: value });
})
.catch((reason: any) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: 902,
error: `REST parse json fail: ${reason}`,
statusMessage: "Fetch parse error",
message: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
} else {
resolve({ status: response.status, data: response.body });
}
} else {
reject({
name: "REST return no OK status",
time: Date().toString(),
status: response.status,
error: `${response.body}`,
statusMessage: "Fetch code error",
message: "rest-tools.ts Wrong return code",
} as RestErrorResponse);
}
})
.catch((error: any) => {
reject({ reject({
name: "Request fail", name: "API serialization error",
time: Date(), time: Date().toString(),
status: 999, status: 902,
error: error, message: `REST parse json fail: ${reason}`,
statusMessage: "Fetch catch error", statusMessage: "Fetch parse error",
message: "rest-tools.ts detect an error in the fetch request", error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
} else {
resolve({ status: response.status, data: response.body });
}
} else {
// the answer is not correct not a 2XX
// clone the response to keep the raw data if case of error:
response
.clone()
.json()
.then((value: any) => {
if (isRestErrorResponse(value)) {
reject(value);
} else {
response
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: 903,
message: `REST parse error json with wrong type fail. ${dataError}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
}
})
.catch((reason: Error) => {
response
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ${dataError} with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT FAIL",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
}); });
}); });
}); }
})
.catch((error: Error) => {
if (isRestErrorResponse(error)) {
reject(error);
} else {
reject({
name: "Request fail",
time: Date(),
status: 999,
message: error,
statusMessage: "Fetch catch error",
error: "rest-tools.ts detect an error in the fetch request",
});
}
});
});
} }
export function RESTRequestJson<TYPE>( export function RESTRequestJson<TYPE>(
request: RESTRequestType, request: RESTRequestType,
checker?: (data: any) => data is TYPE checker?: (data: any) => data is TYPE
): Promise<TYPE> { ): Promise<TYPE> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
RESTRequest(request) RESTRequest(request)
.then((value: ModelResponseHttp) => { .then((value: ModelResponseHttp) => {
if (isNullOrUndefined(checker)) { if (isNullOrUndefined(checker)) {
console.log(`Have no check of MODEL in API: ${RESTUrl(request)}`); console.log(`Have no check of MODEL in API: ${RESTUrl(request)}`);
resolve(value.data); resolve(value.data);
} else if (checker === undefined || checker(value.data)) { } else if (checker === undefined || checker(value.data)) {
resolve(value.data); resolve(value.data);
} else { } else {
reject({ reject({
name: "Model check fail", name: "Model check fail",
time: Date().toString(), time: Date().toString(),
status: 950, status: 950,
error: "REST Fail to verify the data", error: "REST Fail to verify the data",
statusMessage: "API cast ERROR", statusMessage: "API cast ERROR",
message: "api.ts Check type as fail", message: "api.ts Check type as fail",
} as RestErrorResponse); } as RestErrorResponse);
} }
}) })
.catch((reason: RestErrorResponse) => { .catch((reason: RestErrorResponse) => {
reject(reason); reject(reason);
}); });
}); });
} }
export function RESTRequestVoid(request: RESTRequestType): Promise<void> { export function RESTRequestVoid(request: RESTRequestType): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
RESTRequest(request) RESTRequest(request)
.then((value: ModelResponseHttp) => { .then((value: ModelResponseHttp) => {
resolve(); resolve();
}) })
.catch((reason: RestErrorResponse) => { .catch((reason: RestErrorResponse) => {
reject(reason); reject(reason);
}); });
}); });
} }