better swr mocks
This commit is contained in:
parent
45bbe7ba12
commit
b9c24772f5
@ -1,5 +1,7 @@
|
|||||||
export * from "./hooks/index.js";
|
export * from "./hooks/index.js";
|
||||||
export * from "./utils/request.js";
|
export * from "./utils/request.js";
|
||||||
|
export * from "./utils/http-impl.browser.js";
|
||||||
|
export * from "./utils/http-impl.sw.js";
|
||||||
export * from "./utils/observable.js";
|
export * from "./utils/observable.js";
|
||||||
export * from "./context/index.js";
|
export * from "./context/index.js";
|
||||||
export * from "./components/index.js";
|
export * from "./components/index.js";
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
|
import { deprecate } from "util";
|
||||||
|
|
||||||
type HttpMethod =
|
type HttpMethod =
|
||||||
| "get"
|
| "get"
|
||||||
@ -63,6 +64,11 @@ type TestValues = {
|
|||||||
|
|
||||||
const logger = new Logger("testing/mock.ts");
|
const logger = new Logger("testing/mock.ts");
|
||||||
|
|
||||||
|
type MockedResponse = {
|
||||||
|
queryMade: ExpectationValues;
|
||||||
|
expectedQuery?: ExpectationValues;
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class MockEnvironment {
|
export abstract class MockEnvironment {
|
||||||
expectations: Array<ExpectationValues> = [];
|
expectations: Array<ExpectationValues> = [];
|
||||||
queriesMade: Array<ExpectationValues> = [];
|
queriesMade: Array<ExpectationValues> = [];
|
||||||
@ -108,7 +114,7 @@ export abstract class MockEnvironment {
|
|||||||
qparam?: any;
|
qparam?: any;
|
||||||
response?: ResponseType;
|
response?: ResponseType;
|
||||||
},
|
},
|
||||||
): { status: number; payload: ResponseType } | undefined {
|
): MockedResponse {
|
||||||
const queryMade = { query, params, auth: params.auth };
|
const queryMade = { query, params, auth: params.auth };
|
||||||
this.queriesMade.push(queryMade);
|
this.queriesMade.push(queryMade);
|
||||||
const expectedQuery = this.expectations[this.index];
|
const expectedQuery = this.expectations[this.index];
|
||||||
@ -116,11 +122,9 @@ export abstract class MockEnvironment {
|
|||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.info("unexpected query made", queryMade);
|
logger.info("unexpected query made", queryMade);
|
||||||
}
|
}
|
||||||
return undefined;
|
return { queryMade };
|
||||||
}
|
}
|
||||||
const responseCode = this.expectations[this.index].query.code ?? 200;
|
|
||||||
const mockedResponse = this.expectations[this.index].params
|
|
||||||
?.response as ResponseType;
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.info("tracking query made", {
|
logger.info("tracking query made", {
|
||||||
queryMade,
|
queryMade,
|
||||||
@ -128,7 +132,7 @@ export abstract class MockEnvironment {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.index++;
|
this.index++;
|
||||||
return { status: responseCode, payload: mockedResponse };
|
return { queryMade, expectedQuery };
|
||||||
}
|
}
|
||||||
|
|
||||||
public assertJustExpectedRequestWereMade(): AssertStatus {
|
public assertJustExpectedRequestWereMade(): AssertStatus {
|
||||||
|
@ -17,12 +17,17 @@
|
|||||||
import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
|
import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
|
||||||
import { MockEnvironment } from "./mock.js";
|
import { MockEnvironment } from "./mock.js";
|
||||||
import { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
|
import * as swr__internal from "swr/_internal";
|
||||||
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
|
import { buildRequestFailed, RequestError } from "../index.browser.js";
|
||||||
|
|
||||||
|
const logger = new Logger("tests/swr.ts");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for hook that use SWR inside.
|
* Helper for hook that use SWR inside.
|
||||||
*
|
*
|
||||||
* buildTestingContext() will return a testing context
|
* buildTestingContext() will return a testing context
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class SwrMockEnvironment extends MockEnvironment {
|
export class SwrMockEnvironment extends MockEnvironment {
|
||||||
constructor(debug = false) {
|
constructor(debug = false) {
|
||||||
@ -32,47 +37,68 @@ export class SwrMockEnvironment extends MockEnvironment {
|
|||||||
public buildTestingContext(): FunctionalComponent<{
|
public buildTestingContext(): FunctionalComponent<{
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
}> {
|
}> {
|
||||||
const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE = this.saveRequestAndGetMockedResponse.bind(this);
|
const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE =
|
||||||
|
this.saveRequestAndGetMockedResponse.bind(this);
|
||||||
|
|
||||||
|
function testingFetcher(params: any): any {
|
||||||
|
const url = JSON.stringify(params);
|
||||||
|
const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE<any, any>(
|
||||||
|
{
|
||||||
|
method: "get",
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
//unexpected query
|
||||||
|
if (!mocked.expectedQuery) return undefined;
|
||||||
|
const status = mocked.expectedQuery.query.code ?? 200;
|
||||||
|
const requestPayload = mocked.expectedQuery.params?.request;
|
||||||
|
const responsePayload = mocked.expectedQuery.params?.response;
|
||||||
|
//simulated error
|
||||||
|
if (status >= 400) {
|
||||||
|
const error = buildRequestFailed(
|
||||||
|
url,
|
||||||
|
JSON.stringify(responsePayload),
|
||||||
|
status,
|
||||||
|
requestPayload,
|
||||||
|
);
|
||||||
|
//example error handling from https://swr.vercel.app/docs/error-handling
|
||||||
|
throw new RequestError(error);
|
||||||
|
}
|
||||||
|
return responsePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: Partial<swr__internal.PublicConfiguration> & {
|
||||||
|
provider: () => Map<any, any>;
|
||||||
|
} = {
|
||||||
|
use: [
|
||||||
|
(useSWRNext) => {
|
||||||
|
return (key, fetcher, config) => {
|
||||||
|
//prevent the request
|
||||||
|
//use the testing fetcher instead
|
||||||
|
return useSWRNext(key, testingFetcher, config);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fetcher: testingFetcher,
|
||||||
|
//These options are set for ending the test faster
|
||||||
|
//otherwise SWR will create timeouts that will live after the test finished
|
||||||
|
loadingTimeout: 0,
|
||||||
|
dedupingInterval: 0,
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
errorRetryInterval: 0,
|
||||||
|
errorRetryCount: 0,
|
||||||
|
//clean cache for every test
|
||||||
|
provider: () => new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
return function TestingContext({
|
return function TestingContext({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
return h(
|
return h(SWRConfig, { value }, children);
|
||||||
SWRConfig,
|
|
||||||
{
|
|
||||||
value: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
fetcher: (url: string, options: object) => {
|
|
||||||
const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE(
|
|
||||||
{
|
|
||||||
method: "get",
|
|
||||||
url,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
if (!mocked) return undefined;
|
|
||||||
if (mocked.status > 400) {
|
|
||||||
const e: any = Error("simulated error for testing");
|
|
||||||
//example error handling from https://swr.vercel.app/docs/error-handling
|
|
||||||
e.status = mocked.status;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return mocked.payload;
|
|
||||||
},
|
|
||||||
//These options are set for ending the test faster
|
|
||||||
//otherwise SWR will create timeouts that will live after the test finished
|
|
||||||
loadingTimeout: 0,
|
|
||||||
dedupingInterval: 0,
|
|
||||||
shouldRetryOnError: false,
|
|
||||||
errorRetryInterval: 0,
|
|
||||||
errorRetryCount: 0,
|
|
||||||
//clean cache for every test
|
|
||||||
provider: () => new Map(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
203
packages/web-util/src/utils/http-impl.browser.ts
Normal file
203
packages/web-util/src/utils/http-impl.browser.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
Logger,
|
||||||
|
RequestThrottler,
|
||||||
|
TalerErrorCode,
|
||||||
|
TalerError,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
|
import {
|
||||||
|
HttpRequestLibrary,
|
||||||
|
HttpRequestOptions,
|
||||||
|
HttpResponse,
|
||||||
|
Headers,
|
||||||
|
} from "@gnu-taler/taler-util/http";
|
||||||
|
|
||||||
|
const logger = new Logger("browserHttpLib");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of the [[HttpRequestLibrary]] using the
|
||||||
|
* browser's XMLHttpRequest.
|
||||||
|
*/
|
||||||
|
export class BrowserHttpLib implements HttpRequestLibrary {
|
||||||
|
private throttle = new RequestThrottler();
|
||||||
|
private throttlingEnabled = true;
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
requestUrl: string,
|
||||||
|
options?: HttpRequestOptions,
|
||||||
|
): Promise<HttpResponse> {
|
||||||
|
const requestMethod = options?.method ?? "GET";
|
||||||
|
const requestBody = options?.body;
|
||||||
|
|
||||||
|
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
|
||||||
|
const parsedUrl = new URL(requestUrl);
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
|
||||||
|
{
|
||||||
|
requestMethod,
|
||||||
|
requestUrl,
|
||||||
|
throttleStats: this.throttle.getThrottleStats(requestUrl),
|
||||||
|
},
|
||||||
|
`request to origin ${parsedUrl.origin} was throttled`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<HttpResponse>((resolve, reject) => {
|
||||||
|
const myRequest = new XMLHttpRequest();
|
||||||
|
myRequest.open(requestMethod, requestUrl);
|
||||||
|
if (options?.headers) {
|
||||||
|
for (const headerName in options.headers) {
|
||||||
|
myRequest.setRequestHeader(headerName, options.headers[headerName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
myRequest.responseType = "arraybuffer";
|
||||||
|
if (requestBody) {
|
||||||
|
if (requestBody instanceof ArrayBuffer) {
|
||||||
|
myRequest.send(requestBody);
|
||||||
|
} else if (ArrayBuffer.isView(requestBody)) {
|
||||||
|
myRequest.send(requestBody);
|
||||||
|
} else if (typeof requestBody === "string") {
|
||||||
|
myRequest.send(requestBody);
|
||||||
|
} else {
|
||||||
|
myRequest.send(JSON.stringify(requestBody));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
myRequest.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
myRequest.onerror = (e) => {
|
||||||
|
logger.error("http request error");
|
||||||
|
reject(
|
||||||
|
TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_NETWORK_ERROR,
|
||||||
|
{
|
||||||
|
requestUrl,
|
||||||
|
requestMethod,
|
||||||
|
},
|
||||||
|
"Could not make request",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
myRequest.addEventListener("readystatechange", (e) => {
|
||||||
|
if (myRequest.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (myRequest.status === 0) {
|
||||||
|
const exc = TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_NETWORK_ERROR,
|
||||||
|
{
|
||||||
|
requestUrl,
|
||||||
|
requestMethod,
|
||||||
|
},
|
||||||
|
"HTTP request failed (status 0, maybe URI scheme was wrong?)",
|
||||||
|
);
|
||||||
|
reject(exc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const makeText = async (): Promise<string> => {
|
||||||
|
const td = new TextDecoder();
|
||||||
|
return td.decode(myRequest.response);
|
||||||
|
};
|
||||||
|
const makeJson = async (): Promise<any> => {
|
||||||
|
let responseJson;
|
||||||
|
try {
|
||||||
|
const td = new TextDecoder();
|
||||||
|
const responseString = td.decode(myRequest.response);
|
||||||
|
responseJson = JSON.parse(responseString);
|
||||||
|
} catch (e) {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
{
|
||||||
|
requestUrl,
|
||||||
|
requestMethod,
|
||||||
|
httpStatusCode: myRequest.status,
|
||||||
|
},
|
||||||
|
"Invalid JSON from HTTP response",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (responseJson === null || typeof responseJson !== "object") {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
{
|
||||||
|
requestUrl,
|
||||||
|
requestMethod,
|
||||||
|
httpStatusCode: myRequest.status,
|
||||||
|
},
|
||||||
|
"Invalid JSON from HTTP response",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return responseJson;
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = myRequest.getAllResponseHeaders();
|
||||||
|
const arr = headers.trim().split(/[\r\n]+/);
|
||||||
|
|
||||||
|
// Create a map of header names to values
|
||||||
|
const headerMap: Headers = new Headers();
|
||||||
|
arr.forEach(function (line) {
|
||||||
|
const parts = line.split(": ");
|
||||||
|
const headerName = parts.shift();
|
||||||
|
if (!headerName) {
|
||||||
|
logger.warn("skipping invalid header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = parts.join(": ");
|
||||||
|
headerMap.set(headerName, value);
|
||||||
|
});
|
||||||
|
const resp: HttpResponse = {
|
||||||
|
requestUrl: requestUrl,
|
||||||
|
status: myRequest.status,
|
||||||
|
headers: headerMap,
|
||||||
|
requestMethod: requestMethod,
|
||||||
|
json: makeJson,
|
||||||
|
text: makeText,
|
||||||
|
bytes: async () => myRequest.response,
|
||||||
|
};
|
||||||
|
resolve(resp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
|
return this.fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
postJson(
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
opt?: HttpRequestOptions,
|
||||||
|
): Promise<HttpResponse> {
|
||||||
|
return this.fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
}
|
205
packages/web-util/src/utils/http-impl.sw.ts
Normal file
205
packages/web-util/src/utils/http-impl.sw.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
RequestThrottler,
|
||||||
|
TalerErrorCode,
|
||||||
|
TalerError,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Headers,
|
||||||
|
HttpRequestLibrary,
|
||||||
|
HttpRequestOptions,
|
||||||
|
HttpResponse,
|
||||||
|
} from "@gnu-taler/taler-util/http";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of the [[HttpRequestLibrary]] using the
|
||||||
|
* browser's XMLHttpRequest.
|
||||||
|
*/
|
||||||
|
export class ServiceWorkerHttpLib implements HttpRequestLibrary {
|
||||||
|
private throttle = new RequestThrottler();
|
||||||
|
private throttlingEnabled = true;
|
||||||
|
|
||||||
|
async fetch(
|
||||||
|
requestUrl: string,
|
||||||
|
options?: HttpRequestOptions,
|
||||||
|
): Promise<HttpResponse> {
|
||||||
|
const requestMethod = options?.method ?? "GET";
|
||||||
|
const requestBody = options?.body;
|
||||||
|
const requestHeader = options?.headers;
|
||||||
|
const requestTimeout = options?.timeout ?? { d_ms: 2 * 1000 };
|
||||||
|
|
||||||
|
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
|
||||||
|
const parsedUrl = new URL(requestUrl);
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
|
||||||
|
{
|
||||||
|
requestMethod,
|
||||||
|
requestUrl,
|
||||||
|
throttleStats: this.throttle.getThrottleStats(requestUrl),
|
||||||
|
},
|
||||||
|
`request to origin ${parsedUrl.origin} was throttled`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let myBody: BodyInit | undefined = undefined;
|
||||||
|
if (requestBody != null) {
|
||||||
|
if (typeof requestBody === "string") {
|
||||||
|
myBody = requestBody;
|
||||||
|
} else if (requestBody instanceof ArrayBuffer) {
|
||||||
|
myBody = requestBody;
|
||||||
|
} else if (ArrayBuffer.isView(requestBody)) {
|
||||||
|
myBody = requestBody;
|
||||||
|
} else if (typeof requestBody === "object") {
|
||||||
|
myBody = JSON.stringify(requestBody);
|
||||||
|
} else {
|
||||||
|
throw Error("unsupported request body type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
let timeoutId: any | undefined;
|
||||||
|
if (requestTimeout.d_ms !== "forever") {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
controller.abort(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT);
|
||||||
|
}, requestTimeout.d_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
headers: requestHeader,
|
||||||
|
body: myBody,
|
||||||
|
method: requestMethod,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerMap = new Headers();
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headerMap.set(key, value);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
headers: headerMap,
|
||||||
|
status: response.status,
|
||||||
|
requestMethod,
|
||||||
|
requestUrl,
|
||||||
|
json: makeJsonHandler(response, requestUrl, requestMethod),
|
||||||
|
text: makeTextHandler(response, requestUrl, requestMethod),
|
||||||
|
bytes: async () => (await response.blob()).arrayBuffer(),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (controller.signal) {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
controller.signal.reason,
|
||||||
|
{},
|
||||||
|
`request to ${requestUrl} timed out`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
|
return this.fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
postJson(
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
opt?: HttpRequestOptions,
|
||||||
|
): Promise<HttpResponse> {
|
||||||
|
return this.fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTextHandler(
|
||||||
|
response: Response,
|
||||||
|
requestUrl: string,
|
||||||
|
requestMethod: string,
|
||||||
|
) {
|
||||||
|
return async function getJsonFromResponse(): Promise<any> {
|
||||||
|
let respText;
|
||||||
|
try {
|
||||||
|
respText = await response.text();
|
||||||
|
} catch (e) {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
{
|
||||||
|
requestUrl,
|
||||||
|
requestMethod,
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
},
|
||||||
|
"Invalid JSON from HTTP response",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return respText;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeJsonHandler(
|
||||||
|
response: Response,
|
||||||
|
requestUrl: string,
|
||||||
|
requestMethod: string,
|
||||||
|
) {
|
||||||
|
return async function getJsonFromResponse(): Promise<any> {
|
||||||
|
let responseJson;
|
||||||
|
try {
|
||||||
|
responseJson = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
{
|
||||||
|
requestUrl,
|
||||||
|
requestMethod,
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
},
|
||||||
|
"Invalid JSON from HTTP response",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (responseJson === null || typeof responseJson !== "object") {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
{
|
||||||
|
requestUrl,
|
||||||
|
requestMethod,
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
},
|
||||||
|
"Invalid JSON from HTTP response",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return responseJson;
|
||||||
|
};
|
||||||
|
}
|
@ -126,11 +126,12 @@ export async function defaultRequestHandler<T>(
|
|||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
const error = await buildRequestFailed(
|
const dataTxt = await response.text();
|
||||||
response,
|
const error = buildRequestFailed(
|
||||||
_url.href,
|
_url.href,
|
||||||
|
dataTxt,
|
||||||
|
response.status,
|
||||||
payload,
|
payload,
|
||||||
!!options.token,
|
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
throw new RequestError(error);
|
throw new RequestError(error);
|
||||||
@ -292,47 +293,58 @@ async function buildRequestOk<T>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildRequestFailed<ErrorDetail>(
|
export function buildRequestFailed<ErrorDetail>(
|
||||||
response: Response,
|
|
||||||
url: string,
|
url: string,
|
||||||
|
dataTxt: string,
|
||||||
|
status: number,
|
||||||
payload: any,
|
payload: any,
|
||||||
hasToken: boolean,
|
maybeOptions?: RequestOptions,
|
||||||
options: RequestOptions,
|
):
|
||||||
): Promise<
|
|
||||||
| HttpResponseClientError<ErrorDetail>
|
| HttpResponseClientError<ErrorDetail>
|
||||||
| HttpResponseServerError<ErrorDetail>
|
| HttpResponseServerError<ErrorDetail>
|
||||||
| HttpResponseUnreadableError
|
| HttpResponseUnreadableError
|
||||||
| HttpResponseUnexpectedError
|
| HttpResponseUnexpectedError {
|
||||||
> {
|
const options = maybeOptions ?? {};
|
||||||
const status = response?.status;
|
|
||||||
|
|
||||||
const info: RequestInfo = {
|
const info: RequestInfo = {
|
||||||
payload,
|
payload,
|
||||||
url,
|
url,
|
||||||
hasToken,
|
hasToken: !!options.token,
|
||||||
options,
|
options,
|
||||||
status: status || 0,
|
status: status || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataTxt = await response.text();
|
// const dataTxt = await response.text();
|
||||||
try {
|
try {
|
||||||
const data = dataTxt ? JSON.parse(dataTxt) : undefined;
|
const data = dataTxt ? JSON.parse(dataTxt) : undefined;
|
||||||
|
const errorCode = !data || !data.code ? "" : `(code: ${data.code})`;
|
||||||
|
const errorHint =
|
||||||
|
!data || !data.hint ? "Not hint." : `${data.hint} ${errorCode}`;
|
||||||
|
|
||||||
if (status && status >= 400 && status < 500) {
|
if (status && status >= 400 && status < 500) {
|
||||||
|
const message =
|
||||||
|
data === undefined
|
||||||
|
? `Client error (${status}) without data.`
|
||||||
|
: errorHint;
|
||||||
|
|
||||||
const error: HttpResponseClientError<ErrorDetail> = {
|
const error: HttpResponseClientError<ErrorDetail> = {
|
||||||
type: ErrorType.CLIENT,
|
type: ErrorType.CLIENT,
|
||||||
status,
|
status,
|
||||||
info,
|
info,
|
||||||
message: data?.hint,
|
message,
|
||||||
payload: data,
|
payload: data,
|
||||||
};
|
};
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
if (status && status >= 500 && status < 600) {
|
if (status && status >= 500 && status < 600) {
|
||||||
|
const message =
|
||||||
|
data === undefined
|
||||||
|
? `Server error (${status}) without data.`
|
||||||
|
: errorHint;
|
||||||
const error: HttpResponseServerError<ErrorDetail> = {
|
const error: HttpResponseServerError<ErrorDetail> = {
|
||||||
type: ErrorType.SERVER,
|
type: ErrorType.SERVER,
|
||||||
status,
|
status,
|
||||||
info,
|
info,
|
||||||
message: `${data?.hint} (code ${data?.code})`,
|
message,
|
||||||
payload: data,
|
payload: data,
|
||||||
};
|
};
|
||||||
return error;
|
return error;
|
||||||
|
Loading…
Reference in New Issue
Block a user