Cleanup FetchHttpClient

This commit is contained in:
Brennan Conroy 2019-08-28 11:13:53 -07:00 committed by Brennan
parent 2be0ffae19
commit 7317bb16a9
6 changed files with 120 additions and 98 deletions

View File

@ -1,10 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { DefaultHttpClient, FetchHttpClient, HttpClient, HttpTransportType, IHubProtocol, JsonHubProtocol, XhrHttpClient } from "@microsoft/signalr"; import { HttpClient, HttpTransportType, IHubProtocol, JsonHubProtocol } from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { TestLogger } from "./TestLogger"; import { TestLogger } from "./TestLogger";
import { FetchHttpClient } from "@microsoft/signalr/dist/esm/FetchHttpClient";
import { NodeHttpClient } from "@microsoft/signalr/dist/esm/NodeHttpClient";
import { Platform } from "@microsoft/signalr/dist/esm/Utils";
import { XhrHttpClient } from "@microsoft/signalr/dist/esm/XhrHttpClient";
// On slower CI machines, these tests sometimes take longer than 5s // On slower CI machines, these tests sometimes take longer than 5s
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000;
@ -98,14 +103,34 @@ export function eachTransportAndProtocol(action: (transport: HttpTransportType,
}); });
} }
export function eachTransportAndProtocolAndHttpClient(action: (transport: HttpTransportType, protocol: IHubProtocol, httpClient: HttpClient) => void) {
eachTransportAndProtocol((transport, protocol) => {
getHttpClients().forEach((httpClient) => {
action(transport, protocol, httpClient);
});
});
}
export function getGlobalObject(): any { export function getGlobalObject(): any {
return typeof window !== "undefined" ? window : global; return typeof window !== "undefined" ? window : global;
} }
export function eachHttpClient(action: (transport: HttpClient) => void) { export function getHttpClients(): HttpClient[] {
const httpClients: HttpClient[] = [new FetchHttpClient(TestLogger.instance), new XhrHttpClient(TestLogger.instance), new DefaultHttpClient(TestLogger.instance)]; const httpClients: HttpClient[] = [];
if (typeof XMLHttpRequest !== "undefined") {
httpClients.push(new XhrHttpClient(TestLogger.instance));
}
if (typeof fetch !== "undefined") {
httpClients.push(new FetchHttpClient(TestLogger.instance));
}
if (Platform.isNode) {
httpClients.push(new NodeHttpClient(TestLogger.instance));
}
return httpClients;
}
return httpClients.forEach((t) => { export function eachHttpClient(action: (transport: HttpClient) => void) {
return getHttpClients().forEach((t) => {
return action(t); return action(t);
}); });
} }

View File

@ -45,7 +45,7 @@ describe("connection", () => {
eachTransport((transportType) => { eachTransport((transportType) => {
eachHttpClient((httpClient) => { eachHttpClient((httpClient) => {
describe(`over ${HttpTransportType[transportType]}`, () => { describe(`over ${HttpTransportType[transportType]} with ${(httpClient.constructor as any).name}`, () => {
it("can send and receive messages", (done) => { it("can send and receive messages", (done) => {
const message = "Hello World!"; const message = "Hello World!";
// the url should be resolved relative to the document.location.host // the url should be resolved relative to the document.location.host

View File

@ -7,7 +7,7 @@
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr"; import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common"; import { eachTransport, eachTransportAndProtocolAndHttpClient, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common";
import "./LogBannerReporter"; import "./LogBannerReporter";
import { TestLogger } from "./TestLogger"; import { TestLogger } from "./TestLogger";
@ -49,12 +49,12 @@ function getConnectionBuilder(transportType?: HttpTransportType, url?: string, o
} }
describe("hubConnection", () => { describe("hubConnection", () => {
eachTransportAndProtocol((transportType, protocol) => { eachTransportAndProtocolAndHttpClient((transportType, protocol, httpClient) => {
describe("using " + protocol.name + " over " + HttpTransportType[transportType] + " transport", () => { describe("using " + protocol.name + " over " + HttpTransportType[transportType] + " transport", () => {
it("can invoke server method and receive result", (done) => { it("can invoke server method and receive result", (done) => {
const message = "你好,世界!"; const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -81,7 +81,7 @@ describe("hubConnection", () => {
it("using https, can invoke server method and receive result", (done) => { it("using https, can invoke server method and receive result", (done) => {
const message = "你好,世界!"; const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL) const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -108,7 +108,7 @@ describe("hubConnection", () => {
it("can invoke server method non-blocking and not receive result", (done) => { it("can invoke server method non-blocking and not receive result", (done) => {
const message = "你好,世界!"; const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -130,7 +130,7 @@ describe("hubConnection", () => {
}); });
it("can invoke server method structural object and receive structural result", (done) => { it("can invoke server method structural object and receive structural result", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -154,7 +154,7 @@ describe("hubConnection", () => {
}); });
it("can stream server method and receive result", (done) => { it("can stream server method and receive result", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -185,7 +185,7 @@ describe("hubConnection", () => {
}); });
it("can stream server method and cancel stream", (done) => { it("can stream server method and cancel stream", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -219,7 +219,7 @@ describe("hubConnection", () => {
it("rethrows an exception from the server when invoking", (done) => { it("rethrows an exception from the server when invoking", (done) => {
const errorMessage = "An unexpected error occurred invoking 'ThrowException' on the server. InvalidOperationException: An error occurred."; const errorMessage = "An unexpected error occurred invoking 'ThrowException' on the server. InvalidOperationException: An error occurred.";
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -241,7 +241,7 @@ describe("hubConnection", () => {
}); });
it("throws an exception when invoking streaming method with invoke", (done) => { it("throws an exception when invoking streaming method with invoke", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -263,7 +263,7 @@ describe("hubConnection", () => {
}); });
it("throws an exception when receiving a streaming result for method called with invoke", (done) => { it("throws an exception when receiving a streaming result for method called with invoke", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -286,7 +286,7 @@ describe("hubConnection", () => {
it("rethrows an exception from the server when streaming", (done) => { it("rethrows an exception from the server when streaming", (done) => {
const errorMessage = "An unexpected error occurred invoking 'StreamThrowException' on the server. InvalidOperationException: An error occurred."; const errorMessage = "An unexpected error occurred invoking 'StreamThrowException' on the server. InvalidOperationException: An error occurred.";
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -313,7 +313,7 @@ describe("hubConnection", () => {
}); });
it("throws an exception when invoking hub method with stream", (done) => { it("throws an exception when invoking hub method with stream", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -340,7 +340,7 @@ describe("hubConnection", () => {
}); });
it("can receive server calls", (done) => { it("can receive server calls", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -370,7 +370,7 @@ describe("hubConnection", () => {
}); });
it("can receive server calls without rebinding handler when restarted", (done) => { it("can receive server calls without rebinding handler when restarted", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -425,7 +425,7 @@ describe("hubConnection", () => {
}); });
it("closed with error or start fails if hub cannot be created", async (done) => { it("closed with error or start fails if hub cannot be created", async (done) => {
const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable") const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable", { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -446,7 +446,7 @@ describe("hubConnection", () => {
}); });
it("can handle different types", (done) => { it("can handle different types", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -489,7 +489,7 @@ describe("hubConnection", () => {
}); });
it("can receive different types", (done) => { it("can receive different types", (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -534,7 +534,7 @@ describe("hubConnection", () => {
it("can be restarted", (done) => { it("can be restarted", (done) => {
const message = "你好,世界!"; const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -577,7 +577,7 @@ describe("hubConnection", () => {
}); });
it("can stream from client to server with rxjs", async (done) => { it("can stream from client to server with rxjs", async (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();
@ -594,7 +594,7 @@ describe("hubConnection", () => {
}); });
it("can stream from client to server and close with error with rxjs", async (done) => { it("can stream from client to server and close with error with rxjs", async (done) => {
const hubConnection = getConnectionBuilder(transportType) const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol) .withHubProtocol(protocol)
.build(); .build();

View File

@ -14,113 +14,104 @@ export class FetchHttpClient extends HttpClient {
} }
/** @inheritDoc */ /** @inheritDoc */
public send(request: HttpRequest): Promise<HttpResponse> { public async send(request: HttpRequest): Promise<HttpResponse> {
// Check that abort was not signaled before calling send // Check that abort was not signaled before calling send
if (request.abortSignal && request.abortSignal.aborted) { if (request.abortSignal && request.abortSignal.aborted) {
return Promise.reject(new AbortError()); throw new AbortError();
} }
if (!request.method) { if (!request.method) {
return Promise.reject(new Error("No method defined.")); throw new Error("No method defined.");
} }
if (!request.url) { if (!request.url) {
return Promise.reject(new Error("No url defined.")); throw new Error("No url defined.");
} }
return new Promise<HttpResponse>((resolve, reject) => { const abortController = new AbortController();
const abortController = new AbortController();
const fetchRequest = new Request(request.url!, { let error: any;
// Hook our abortSignal into the abort controller
if (request.abortSignal) {
request.abortSignal.onabort = () => {
abortController.abort();
error = 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();
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
error = new TimeoutError();
}, msTimeout);
}
let response: Response;
try {
response = await fetch(request.url!, {
body: request.content!, body: request.content!,
cache: "no-cache", cache: "no-cache",
credentials: "include", credentials: "include",
headers: { headers: {
"Content-Type": "text/plain;charset=UTF-8", "Content-Type": "text/plain;charset=UTF-8",
"X-Requested-With": "Fetch", "X-Requested-With": "XMLHttpRequest",
...request.headers, ...request.headers,
}, },
method: request.method!, method: request.method!,
mode: "cors", mode: "cors",
redirect: "manual",
signal: abortController.signal, signal: abortController.signal,
}); });
} catch (e) {
// Hook our abourtSignal into the abort controller if (error) {
throw error;
}
this.logger.log(
LogLevel.Warning,
`Error from HTTP request. ${e}.`,
);
throw e;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (request.abortSignal) { if (request.abortSignal) {
request.abortSignal.onabort = () => { request.abortSignal.onabort = null;
abortController.abort();
reject(new AbortError());
};
} }
}
// If a timeout has been passed in setup a timeout to call abort if (!response.ok) {
// Type needs to be any to fit window.setTimeout and NodeJS.setTimeout throw new HttpError(response.statusText, response.status);
let timeoutId: any = null; }
if (request.timeout) {
const msTimeout = request.timeout!;
timeoutId = setTimeout(() => {
abortController.abort();
reject(new TimeoutError());
}, msTimeout);
}
fetch(fetchRequest) const content = deserializeContent(response, request.responseType);
.then((response: Response) => { const payload = await content;
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); return new HttpResponse(
response.status,
content.then((payload) => { response.statusText,
resolve(new HttpResponse( payload,
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<any> { function deserializeContent(response: Response, responseType?: XMLHttpRequestResponseType): Promise<string | ArrayBuffer> {
let content; let content;
switch (responseType) { switch (responseType) {
case "arraybuffer": case "arraybuffer":
content = response.arrayBuffer(); content = response.arrayBuffer();
break; break;
case "blob":
content = response.blob();
break;
case "document":
content = response.json();
break;
case "json":
content = response.json();
break;
case "text": case "text":
content = response.text(); content = response.text();
break; break;
case "blob":
case "document":
case "json":
throw new Error(`${responseType} is not supported.`);
default: default:
content = response.text(); content = response.text();
break; break;

View File

@ -57,6 +57,14 @@ export class HttpResponse {
* @param {ArrayBuffer} content The content of the response. * @param {ArrayBuffer} content The content of the response.
*/ */
constructor(statusCode: number, statusText: string, content: ArrayBuffer); constructor(statusCode: number, statusText: string, content: ArrayBuffer);
/** Constructs a new instance of {@link @microsoft/signalr.HttpResponse} with the specified status code, message and binary content.
*
* @param {number} statusCode The status code of the response.
* @param {string} statusText The status message of the response.
* @param {string | ArrayBuffer} content The content of the response.
*/
constructor(statusCode: number, statusText: string, content: string | ArrayBuffer);
constructor( constructor(
public readonly statusCode: number, public readonly statusCode: number,
public readonly statusText?: string, public readonly statusText?: string,

View File

@ -10,7 +10,6 @@ export { AbortSignal } from "./AbortController";
export { AbortError, HttpError, TimeoutError } from "./Errors"; export { AbortError, HttpError, TimeoutError } from "./Errors";
export { HttpClient, HttpRequest, HttpResponse } from "./HttpClient"; export { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
export { DefaultHttpClient } from "./DefaultHttpClient"; export { DefaultHttpClient } from "./DefaultHttpClient";
export { FetchHttpClient } from "./FetchHttpClient";
export { IHttpConnectionOptions } from "./IHttpConnectionOptions"; export { IHttpConnectionOptions } from "./IHttpConnectionOptions";
export { HubConnection, HubConnectionState } from "./HubConnection"; export { HubConnection, HubConnectionState } from "./HubConnection";
export { HubConnectionBuilder } from "./HubConnectionBuilder"; export { HubConnectionBuilder } from "./HubConnectionBuilder";
@ -23,4 +22,3 @@ export { NullLogger } from "./Loggers";
export { JsonHubProtocol } from "./JsonHubProtocol"; export { JsonHubProtocol } from "./JsonHubProtocol";
export { Subject } from "./Subject"; export { Subject } from "./Subject";
export { IRetryPolicy, RetryContext } from "./IRetryPolicy"; export { IRetryPolicy, RetryContext } from "./IRetryPolicy";
export { XhrHttpClient } from "./XhrHttpClient";