From 71a8092cb9ca270672b441b19d263c339c62379f Mon Sep 17 00:00:00 2001 From: Harley Adams Date: Thu, 25 Jul 2019 11:20:59 -0700 Subject: [PATCH] Add FetchHttpClient --- .../ts/signalr/src/DefaultHttpClient.ts | 5 +- .../clients/ts/signalr/src/FetchHttpClient.ts | 130 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts diff --git a/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts b/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts index fece43020d..8058e5716a 100644 --- a/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts +++ b/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. import { AbortError } from "./Errors"; +import { FetchHttpClient } from "./FetchHttpClient"; import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient"; import { ILogger } from "./ILogger"; import { NodeHttpClient } from "./NodeHttpClient"; @@ -15,7 +16,9 @@ export class DefaultHttpClient extends HttpClient { public constructor(logger: ILogger) { super(); - if (typeof XMLHttpRequest !== "undefined") { + if (typeof fetch !== "undefined") { + this.httpClient = new FetchHttpClient(logger); + } else if (typeof XMLHttpRequest !== "undefined") { this.httpClient = new XhrHttpClient(logger); } else { this.httpClient = new NodeHttpClient(logger); diff --git a/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts b/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts new file mode 100644 index 0000000000..0b4c2754a7 --- /dev/null +++ b/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +import { AbortError, HttpError, TimeoutError } from "./Errors"; +import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient"; +import { ILogger, LogLevel } from "./ILogger"; + +export class FetchHttpClient extends HttpClient { + private readonly logger: ILogger; + + public constructor(logger: ILogger) { + super(); + this.logger = logger; + } + + /** @inheritDoc */ + public send(request: HttpRequest): Promise { + // Check that abort was not signaled before calling send + if (request.abortSignal && request.abortSignal.aborted) { + return Promise.reject(new AbortError()); + } + + if (!request.method) { + return Promise.reject(new Error("No method defined.")); + } + if (!request.url) { + return Promise.reject(new Error("No url defined.")); + } + + return new Promise((resolve, reject) => { + const abortController = new AbortController(); + + const fetchRequest = new Request(request.url!, { + body: request.content!, + cache: "no-cache", + credentials: "include", + headers: { + "Content-Type": "text/plain;charset=UTF-8", + "X-Requested-With": "Fetch", + ...request.headers, + }, + method: request.method!, + mode: "cors", + signal: abortController.signal, + }); + + // Hook our abourtSignal into the abort controller + if (request.abortSignal) { + request.abortSignal.onabort = () => { + abortController.abort(); + reject(new AbortError()); + }; + } + + // If a timeout has been passed in setup a timeout to call abort + // Type needs to be any to fit window.setTimeout and NodeJS.setTimeout + let timeoutId: any = null; + if (request.timeout) { + const msTimeout = request.timeout!; + timeoutId = setTimeout(() => { + abortController.abort(); + reject(new TimeoutError()); + }, msTimeout); + } + + fetch(fetchRequest) + .then((response: Response) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (!response.ok) { + throw new Error(`${response.status}: ${response.statusText}.`); + } else { + return response; + } + }) + .then((response: Response) => { + if (request.abortSignal) { + request.abortSignal.onabort = null; + } + + const content = deserializeContent(response, request.responseType); + + content.then((payload) => { + resolve(new HttpResponse( + response.status, + response.statusText, + payload, + )); + }).catch(() => { + reject(new HttpError(response.statusText, response.status)); + }); + }) + .catch((error) => { + this.logger.log( + LogLevel.Warning, + `Error from HTTP request. ${error.message}.`, + ); + const [statusText, status] = error.message.split(":"); + reject(new HttpError(statusText, status)); + }); + }); + } +} + +function deserializeContent(response: Response, responseType?: XMLHttpRequestResponseType): Promise { + let content; + switch (responseType) { + case "arraybuffer": + content = response.arrayBuffer(); + break; + case "blob": + content = response.blob(); + break; + case "document": + content = response.json(); + break; + case "json": + content = response.json(); + break; + case "text": + content = response.text(); + break; + default: + content = response.text(); + break; + } + + return content; +}