diff options
Diffstat (limited to 'packages/web-util/src/utils/http-impl.sw.ts')
-rw-r--r-- | packages/web-util/src/utils/http-impl.sw.ts | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts new file mode 100644 index 000000000..9c820bb4b --- /dev/null +++ b/packages/web-util/src/utils/http-impl.sw.ts @@ -0,0 +1,217 @@ +/* + 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 { + Duration, + RequestThrottler, + TalerError, + TalerErrorCode +} from "@gnu-taler/taler-util"; + +import { + DEFAULT_REQUEST_TIMEOUT_MS, + Headers, + HttpLibArgs, + HttpRequestLibrary, + HttpRequestOptions, + HttpResponse, + encodeBody, + getDefaultHeaders, +} from "@gnu-taler/taler-util/http"; + +/** + * An implementation of the [[HttpRequestLibrary]] using the + * browser's XMLHttpRequest. + */ +export class BrowserFetchHttpLib implements HttpRequestLibrary { + private throttle = new RequestThrottler(); + private throttlingEnabled = true; + private requireTls = false; + + public constructor(args?: HttpLibArgs) { + this.throttlingEnabled = args?.enableThrottling ?? true; + this.requireTls = args?.requireTls ?? false; + } + + 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 ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS); + const requestCancel = options?.cancellationToken; + const requestRedirect = options?.redirect; + + const parsedUrl = new URL(requestUrl); + if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, + { + requestMethod, + requestUrl, + throttleStats: this.throttle.getThrottleStats(requestUrl), + }, + `request to origin ${parsedUrl.origin} was throttled`, + ); + } + if (this.requireTls && parsedUrl.protocol !== "https:") { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_NETWORK_ERROR, + { + requestMethod: requestMethod, + requestUrl: requestUrl, + }, + `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`, + ); + } + + const myBody: ArrayBuffer | undefined = + requestMethod === "POST" || requestMethod === "PUT" || requestMethod === "PATCH" + ? encodeBody(requestBody) + : undefined; + + const requestHeadersMap = getDefaultHeaders(requestMethod); + if (requestHeader) { + Object.entries(requestHeader).forEach(([key, value]) => { + if (value === undefined) return; + requestHeadersMap[key] = value + }) + } + + const controller = new AbortController(); + let timeoutId: ReturnType<typeof setTimeout> | undefined; + if (requestTimeout.d_ms !== "forever") { + timeoutId = setTimeout(() => { + controller.abort(TalerErrorCode.GENERIC_TIMEOUT); + }, requestTimeout.d_ms); + } + if (requestCancel) { + requestCancel.onCancelled(() => { + controller.abort(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR) + }); + } + + try { + const response = await fetch(requestUrl, { + headers: requestHeadersMap, + body: myBody, + method: requestMethod, + signal: controller.signal, + redirect: requestRedirect + }); + + 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, + { + requestUrl, + requestMethod, + timeoutMs: requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms + }, + `HTTP request failed.`, + ); + } + throw e; + } + } + +} + +function makeTextHandler( + response: Response, + requestUrl: string, + requestMethod: string, +) { + return async function getTextFromResponse(): 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 text from HTTP response", + ); + } + return respText; + }; +} + +function makeJsonHandler( + response: Response, + requestUrl: string, + requestMethod: string, +) { + let responseJson: unknown = undefined; + return async function getJsonFromResponse(): Promise<any> { + if (responseJson === undefined) { + try { + responseJson = await response.json(); + } catch (e) { + const message = e instanceof Error ? `Invalid JSON from HTTP response: ${e.message}` : "Invalid JSON from HTTP response" + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + requestUrl, + requestMethod, + httpStatusCode: response.status, + }, + message, + ); + } + } + if (responseJson === null || typeof responseJson !== "object") { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + requestUrl, + requestMethod, + httpStatusCode: response.status, + }, + "Invalid JSON from HTTP response: null or not object", + ); + } + return responseJson; + }; +} |