Add custom header support to Typescript client (#19227)

This commit is contained in:
Sergey Bakulin 2020-03-28 01:08:34 +03:00 committed by GitHub
parent d225883c7d
commit 3fe9012821
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 224 additions and 45 deletions

View File

@ -1123,6 +1123,34 @@ describe("hubConnection", () => {
fail(e); fail(e);
} }
}); });
it("overwrites library headers with user headers", async (done) => {
const [name] = getUserAgentHeader();
const headers = { [name]: "Custom Agent", "X-HEADER": "VALUE" };
const hubConnection = getConnectionBuilder(t, TESTHUBENDPOINT_URL, { headers })
.withHubProtocol(new JsonHubProtocol())
.build();
try {
await hubConnection.start();
const customUserHeader = await hubConnection.invoke("GetHeader", "X-HEADER");
const headerValue = await hubConnection.invoke("GetHeader", name);
if ((t === HttpTransportType.ServerSentEvents || t === HttpTransportType.WebSockets) && !Platform.isNode) {
expect(headerValue).toBeNull();
expect(customUserHeader).toBeNull();
} else {
expect(headerValue).toEqual("Custom Agent");
expect(customUserHeader).toEqual("VALUE");
}
await hubConnection.stop();
done();
} catch (e) {
fail(e);
}
});
}); });
function getJwtToken(url: string): Promise<string> { function getJwtToken(url: string): Promise<string> {

View File

@ -36,7 +36,7 @@ To use the client in a NodeJS application, install the package to your `node_mod
### Example (Browser) ### Example (Browser)
```JavaScript ```javascript
let connection = new signalR.HubConnectionBuilder() let connection = new signalR.HubConnectionBuilder()
.withUrl("/chat") .withUrl("/chat")
.build(); .build();
@ -51,8 +51,7 @@ connection.start()
### Example (WebWorker) ### Example (WebWorker)
```javascript
```JavaScript
importScripts('signalr.js'); importScripts('signalr.js');
let connection = new signalR.HubConnectionBuilder() let connection = new signalR.HubConnectionBuilder()
@ -70,7 +69,7 @@ connection.start()
### Example (NodeJS) ### Example (NodeJS)
```JavaScript ```javascript
const signalR = require("@microsoft/signalr"); const signalR = require("@microsoft/signalr");
let connection = new signalR.HubConnectionBuilder() let connection = new signalR.HubConnectionBuilder()

View File

@ -2,6 +2,7 @@
// 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 { AbortSignal } from "./AbortController"; import { AbortSignal } from "./AbortController";
import { MessageHeaders } from "./IHubProtocol";
/** Represents an HTTP request. */ /** Represents an HTTP request. */
export interface HttpRequest { export interface HttpRequest {
@ -15,7 +16,7 @@ export interface HttpRequest {
content?: string | ArrayBuffer; content?: string | ArrayBuffer;
/** An object describing headers to apply to the request. */ /** An object describing headers to apply to the request. */
headers?: { [key: string]: string }; headers?: MessageHeaders;
/** The XMLHttpRequestResponseType to apply to the request. */ /** The XMLHttpRequestResponseType to apply to the request. */
responseType?: XMLHttpRequestResponseType; responseType?: XMLHttpRequestResponseType;

View File

@ -315,7 +315,7 @@ export class HttpConnection implements IConnection {
try { try {
const response = await this.httpClient.post(negotiateUrl, { const response = await this.httpClient.post(negotiateUrl, {
content: "", content: "",
headers, headers: { ...headers, ...this.options.headers },
withCredentials: this.options.withCredentials, withCredentials: this.options.withCredentials,
}); });
@ -403,14 +403,14 @@ export class HttpConnection implements IConnection {
if (!this.options.WebSocket) { if (!this.options.WebSocket) {
throw new Error("'WebSocket' is not supported in your environment."); throw new Error("'WebSocket' is not supported in your environment.");
} }
return new WebSocketTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.WebSocket); return new WebSocketTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.WebSocket, this.options.headers || {});
case HttpTransportType.ServerSentEvents: case HttpTransportType.ServerSentEvents:
if (!this.options.EventSource) { if (!this.options.EventSource) {
throw new Error("'EventSource' is not supported in your environment."); throw new Error("'EventSource' is not supported in your environment.");
} }
return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.EventSource, this.options.withCredentials!); return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.EventSource, this.options.withCredentials!, this.options.headers || {});
case HttpTransportType.LongPolling: case HttpTransportType.LongPolling:
return new LongPollingTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.withCredentials!); return new LongPollingTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.withCredentials!, this.options.headers || {});
default: default:
throw new Error(`Unknown transport: ${transport}.`); throw new Error(`Unknown transport: ${transport}.`);
} }

View File

@ -2,12 +2,16 @@
// 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 { HttpClient } from "./HttpClient"; import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
import { HttpTransportType, ITransport } from "./ITransport"; import { HttpTransportType, ITransport } from "./ITransport";
import { EventSourceConstructor, WebSocketConstructor } from "./Polyfills"; import { EventSourceConstructor, WebSocketConstructor } from "./Polyfills";
/** Options provided to the 'withUrl' method on {@link @microsoft/signalr.HubConnectionBuilder} to configure options for the HTTP-based transports. */ /** Options provided to the 'withUrl' method on {@link @microsoft/signalr.HubConnectionBuilder} to configure options for the HTTP-based transports. */
export interface IHttpConnectionOptions { export interface IHttpConnectionOptions {
/** {@link @microsoft/signalr.MessageHeaders} containing custom headers to be sent with every HTTP request. Note, setting headers in the browser will not work for WebSockets or the ServerSentEvents stream. */
headers?: MessageHeaders;
/** An {@link @microsoft/signalr.HttpClient} that will be used to make HTTP requests. */ /** An {@link @microsoft/signalr.HttpClient} that will be used to make HTTP requests. */
httpClient?: HttpClient; httpClient?: HttpClient;

View File

@ -4,6 +4,7 @@
import { AbortController } from "./AbortController"; import { AbortController } from "./AbortController";
import { HttpError, TimeoutError } from "./Errors"; import { HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest } from "./HttpClient"; import { HttpClient, HttpRequest } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport"; import { ITransport, TransferFormat } from "./ITransport";
import { Arg, getDataDetail, getUserAgentHeader, sendMessage } from "./Utils"; import { Arg, getDataDetail, getUserAgentHeader, sendMessage } from "./Utils";
@ -17,6 +18,7 @@ export class LongPollingTransport implements ITransport {
private readonly logMessageContent: boolean; private readonly logMessageContent: boolean;
private readonly withCredentials: boolean; private readonly withCredentials: boolean;
private readonly pollAbort: AbortController; private readonly pollAbort: AbortController;
private readonly headers: MessageHeaders;
private url?: string; private url?: string;
private running: boolean; private running: boolean;
@ -31,13 +33,14 @@ export class LongPollingTransport implements ITransport {
return this.pollAbort.aborted; return this.pollAbort.aborted;
} }
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean, withCredentials: boolean) { constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean, withCredentials: boolean, headers: MessageHeaders) {
this.httpClient = httpClient; this.httpClient = httpClient;
this.accessTokenFactory = accessTokenFactory; this.accessTokenFactory = accessTokenFactory;
this.logger = logger; this.logger = logger;
this.pollAbort = new AbortController(); this.pollAbort = new AbortController();
this.logMessageContent = logMessageContent; this.logMessageContent = logMessageContent;
this.withCredentials = withCredentials; this.withCredentials = withCredentials;
this.headers = headers;
this.running = false; this.running = false;
@ -60,9 +63,8 @@ export class LongPollingTransport implements ITransport {
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported."); throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
} }
const headers = {};
const [name, value] = getUserAgentHeader(); const [name, value] = getUserAgentHeader();
headers[name] = value; const headers = { [name]: value, ...this.headers };
const pollOptions: HttpRequest = { const pollOptions: HttpRequest = {
abortSignal: this.pollAbort.signal, abortSignal: this.pollAbort.signal,
@ -185,7 +187,7 @@ export class LongPollingTransport implements ITransport {
if (!this.running) { if (!this.running) {
return Promise.reject(new Error("Cannot send until the transport is connected")); return Promise.reject(new Error("Cannot send until the transport is connected"));
} }
return sendMessage(this.logger, "LongPolling", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials); return sendMessage(this.logger, "LongPolling", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials, this.headers);
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
@ -206,7 +208,7 @@ export class LongPollingTransport implements ITransport {
headers[name] = value; headers[name] = value;
const deleteOptions: HttpRequest = { const deleteOptions: HttpRequest = {
headers, headers: { ...headers, ...this.headers },
withCredentials: this.withCredentials, withCredentials: this.withCredentials,
}; };
const token = await this.getAccessToken(); const token = await this.getAccessToken();

View File

@ -2,6 +2,7 @@
// 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 { HttpClient } from "./HttpClient"; import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport"; import { ITransport, TransferFormat } from "./ITransport";
import { EventSourceConstructor } from "./Polyfills"; import { EventSourceConstructor } from "./Polyfills";
@ -17,18 +18,20 @@ export class ServerSentEventsTransport implements ITransport {
private readonly eventSourceConstructor: EventSourceConstructor; private readonly eventSourceConstructor: EventSourceConstructor;
private eventSource?: EventSource; private eventSource?: EventSource;
private url?: string; private url?: string;
private headers: MessageHeaders;
public onreceive: ((data: string | ArrayBuffer) => void) | null; public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null; public onclose: ((error?: Error) => void) | null;
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger,
logMessageContent: boolean, eventSourceConstructor: EventSourceConstructor, withCredentials: boolean) { logMessageContent: boolean, eventSourceConstructor: EventSourceConstructor, withCredentials: boolean, headers: MessageHeaders) {
this.httpClient = httpClient; this.httpClient = httpClient;
this.accessTokenFactory = accessTokenFactory; this.accessTokenFactory = accessTokenFactory;
this.logger = logger; this.logger = logger;
this.logMessageContent = logMessageContent; this.logMessageContent = logMessageContent;
this.withCredentials = withCredentials; this.withCredentials = withCredentials;
this.eventSourceConstructor = eventSourceConstructor; this.eventSourceConstructor = eventSourceConstructor;
this.headers = headers;
this.onreceive = null; this.onreceive = null;
this.onclose = null; this.onclose = null;
@ -64,13 +67,12 @@ export class ServerSentEventsTransport implements ITransport {
} else { } else {
// Non-browser passes cookies via the dictionary // Non-browser passes cookies via the dictionary
const cookies = this.httpClient.getCookieString(url); const cookies = this.httpClient.getCookieString(url);
const headers = { const headers: MessageHeaders = {};
Cookie: cookies, headers.Cookie = cookies;
};
const [name, value] = getUserAgentHeader(); const [name, value] = getUserAgentHeader();
headers[name] = value; headers[name] = value;
eventSource = new this.eventSourceConstructor(url, { withCredentials: this.withCredentials, headers } as EventSourceInit); eventSource = new this.eventSourceConstructor(url, { withCredentials: this.withCredentials, headers: { ...headers, ...this.headers} } as EventSourceInit);
} }
try { try {
@ -112,7 +114,7 @@ export class ServerSentEventsTransport implements ITransport {
if (!this.eventSource) { if (!this.eventSource) {
return Promise.reject(new Error("Cannot send until the transport is connected")); return Promise.reject(new Error("Cannot send until the transport is connected"));
} }
return sendMessage(this.logger, "SSE", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials); return sendMessage(this.logger, "SSE", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials, this.headers);
} }
public stop(): Promise<void> { public stop(): Promise<void> {

View File

@ -2,6 +2,7 @@
// 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 { HttpClient } from "./HttpClient"; import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
import { NullLogger } from "./Loggers"; import { NullLogger } from "./Loggers";
import { IStreamSubscriber, ISubscription } from "./Stream"; import { IStreamSubscriber, ISubscription } from "./Stream";
@ -85,7 +86,7 @@ export function isArrayBuffer(val: any): val is ArrayBuffer {
/** @private */ /** @private */
export async function sendMessage(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: (() => string | Promise<string>) | undefined, export async function sendMessage(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: (() => string | Promise<string>) | undefined,
content: string | ArrayBuffer, logMessageContent: boolean, withCredentials: boolean): Promise<void> { content: string | ArrayBuffer, logMessageContent: boolean, withCredentials: boolean, defaultHeaders: MessageHeaders): Promise<void> {
let headers = {}; let headers = {};
if (accessTokenFactory) { if (accessTokenFactory) {
const token = await accessTokenFactory(); const token = await accessTokenFactory();
@ -104,7 +105,7 @@ export async function sendMessage(logger: ILogger, transportName: string, httpCl
const responseType = isArrayBuffer(content) ? "arraybuffer" : "text"; const responseType = isArrayBuffer(content) ? "arraybuffer" : "text";
const response = await httpClient.post(url, { const response = await httpClient.post(url, {
content, content,
headers, headers: { ...headers, ...defaultHeaders},
responseType, responseType,
withCredentials, withCredentials,
}); });

View File

@ -2,6 +2,7 @@
// 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 { HttpClient } from "./HttpClient"; import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport"; import { ITransport, TransferFormat } from "./ITransport";
import { WebSocketConstructor } from "./Polyfills"; import { WebSocketConstructor } from "./Polyfills";
@ -15,12 +16,13 @@ export class WebSocketTransport implements ITransport {
private readonly webSocketConstructor: WebSocketConstructor; private readonly webSocketConstructor: WebSocketConstructor;
private readonly httpClient: HttpClient; private readonly httpClient: HttpClient;
private webSocket?: WebSocket; private webSocket?: WebSocket;
private headers: MessageHeaders;
public onreceive: ((data: string | ArrayBuffer) => void) | null; public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null; public onclose: ((error?: Error) => void) | null;
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger,
logMessageContent: boolean, webSocketConstructor: WebSocketConstructor) { logMessageContent: boolean, webSocketConstructor: WebSocketConstructor, headers: MessageHeaders) {
this.logger = logger; this.logger = logger;
this.accessTokenFactory = accessTokenFactory; this.accessTokenFactory = accessTokenFactory;
this.logMessageContent = logMessageContent; this.logMessageContent = logMessageContent;
@ -29,6 +31,7 @@ export class WebSocketTransport implements ITransport {
this.onreceive = null; this.onreceive = null;
this.onclose = null; this.onclose = null;
this.headers = headers;
} }
public async connect(url: string, transferFormat: TransferFormat): Promise<void> { public async connect(url: string, transferFormat: TransferFormat): Promise<void> {
@ -59,9 +62,9 @@ export class WebSocketTransport implements ITransport {
headers[`Cookie`] = `${cookies}`; headers[`Cookie`] = `${cookies}`;
} }
// Only pass cookies when in non-browser environments // Only pass headers when in non-browser environments
webSocket = new this.webSocketConstructor(url, undefined, { webSocket = new this.webSocketConstructor(url, undefined, {
headers, headers: { ...headers, ...this.headers },
}); });
} }

View File

@ -44,9 +44,11 @@ describe("HttpClient", () => {
}); });
await testClient.get("http://localhost", { await testClient.get("http://localhost", {
headers: { "X-HEADER": "VALUE"},
timeout: 42, timeout: 42,
}); });
expect(request.timeout).toEqual(42); expect(request.timeout).toEqual(42);
expect(request.headers).toEqual({ "X-HEADER": "VALUE"});
}); });
}); });
@ -86,9 +88,11 @@ describe("HttpClient", () => {
}); });
await testClient.post("http://localhost", { await testClient.post("http://localhost", {
headers: { "X-HEADER": "VALUE"},
timeout: 42, timeout: 42,
}); });
expect(request.timeout).toEqual(42); expect(request.timeout).toEqual(42);
expect(request.headers).toEqual({ "X-HEADER": "VALUE"});
}); });
}); });
}); });

View File

@ -1152,6 +1152,30 @@ describe("HttpConnection", () => {
}, "Failed to start the connection: Error: nope"); }, "Failed to start the connection: Error: nope");
}); });
it("overwrites library headers with user headers on negotiate", async () => {
await VerifyLogger.run(async (logger) => {
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
const options: IHttpConnectionOptions = {
...commonOptions,
headers,
httpClient: new TestHttpClient()
.on("POST", (r) => {
expect(r.headers).toEqual(headers);
return new HttpResponse(200, "", "{\"error\":\"nope\"}");
}),
logger,
};
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
} catch {
} finally {
await connection.stop();
}
}, "Failed to start the connection: Error: nope");
});
it("logMessageContent displays correctly with binary data", async () => { it("logMessageContent displays correctly with binary data", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const availableTransport = { transport: "LongPolling", transferFormats: ["Text", "Binary"] }; const availableTransport = { transport: "LongPolling", transferFormats: ["Text", "Binary"] };

View File

@ -24,7 +24,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(200); return new HttpResponse(200);
} else { } else {
// Turn 'onabort' into a promise. // Turn 'onabort' into a promise.
const abort = new Promise((resolve, reject) => { const abort = new Promise((resolve) => {
if (r.abortSignal!.aborted) { if (r.abortSignal!.aborted) {
resolve(); resolve();
} else { } else {
@ -40,7 +40,7 @@ describe("LongPollingTransport", () => {
} }
}) })
.on("DELETE", () => new HttpResponse(202)); .on("DELETE", () => new HttpResponse(202));
const transport = new LongPollingTransport(client, undefined, logger, false, true); const transport = new LongPollingTransport(client, undefined, logger, false, true, {});
await transport.connect("http://example.com", TransferFormat.Text); await transport.connect("http://example.com", TransferFormat.Text);
const stopPromise = transport.stop(); const stopPromise = transport.stop();
@ -64,7 +64,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(204); return new HttpResponse(204);
} }
}); });
const transport = new LongPollingTransport(client, undefined, logger, false, true); const transport = new LongPollingTransport(client, undefined, logger, false, true, {});
const stopPromise = makeClosedPromise(transport); const stopPromise = makeClosedPromise(transport);
@ -82,7 +82,7 @@ describe("LongPollingTransport", () => {
const pollingPromiseSource = new PromiseSource(); const pollingPromiseSource = new PromiseSource();
const deleteSyncPoint = new SyncPoint(); const deleteSyncPoint = new SyncPoint();
const httpClient = new TestHttpClient() const httpClient = new TestHttpClient()
.on("GET", async (r) => { .on("GET", async () => {
if (firstPoll) { if (firstPoll) {
firstPoll = false; firstPoll = false;
return new HttpResponse(200); return new HttpResponse(200);
@ -91,13 +91,13 @@ describe("LongPollingTransport", () => {
return new HttpResponse(204); return new HttpResponse(204);
} }
}) })
.on("DELETE", async (r) => { .on("DELETE", async () => {
deleteSent = true; deleteSent = true;
await deleteSyncPoint.waitToContinue(); await deleteSyncPoint.waitToContinue();
return new HttpResponse(202); return new HttpResponse(202);
}); });
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true); const transport = new LongPollingTransport(httpClient, undefined, logger, false, true, {});
await transport.connect("http://tempuri.org", TransferFormat.Text); await transport.connect("http://tempuri.org", TransferFormat.Text);
@ -146,7 +146,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(202); return new HttpResponse(202);
}); });
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true); const transport = new LongPollingTransport(httpClient, undefined, logger, false, true, {});
await transport.connect("http://tempuri.org", TransferFormat.Text); await transport.connect("http://tempuri.org", TransferFormat.Text);
@ -165,6 +165,67 @@ describe("LongPollingTransport", () => {
expect(secondPollUserAgent).toEqual(value); expect(secondPollUserAgent).toEqual(value);
}); });
}); });
it("overwrites library headers with user headers", async () => {
await VerifyLogger.run(async (logger) => {
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
let firstPoll = true;
let firstPollUserAgent = "";
let firstUserHeader = "";
let secondPollUserAgent = "";
let secondUserHeader = "";
let deleteUserAgent = "";
let deleteUserHeader = "";
const pollingPromiseSource = new PromiseSource();
const httpClient = new TestHttpClient()
.on("POST", async (r) => {
expect(r.content).toEqual({ message: "hello" });
expect(r.headers).toEqual(headers);
expect(r.method).toEqual("POST");
expect(r.url).toEqual("http://tempuri.org");
})
.on("GET", async (r) => {
if (firstPoll) {
firstPoll = false;
firstPollUserAgent = r.headers!["User-Agent"];
firstUserHeader = r.headers!["X-HEADER"];
return new HttpResponse(200);
} else {
secondPollUserAgent = r.headers!["User-Agent"];
secondUserHeader = r.headers!["X-HEADER"];
await pollingPromiseSource.promise;
return new HttpResponse(204);
}
})
.on("DELETE", async (r) => {
deleteUserAgent = r.headers!["User-Agent"];
deleteUserHeader = r.headers!["X-HEADER"];
return new HttpResponse(202);
});
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true, headers);
await transport.connect("http://tempuri.org", TransferFormat.Text);
await transport.send({ message: "hello" });
// Begin stopping transport
const stopPromise = transport.stop();
// Allow polling to complete
pollingPromiseSource.resolve();
// Wait for stop to complete
await stopPromise;
expect(firstPollUserAgent).toEqual("Custom Agent");
expect(deleteUserAgent).toEqual("Custom Agent");
expect(secondPollUserAgent).toEqual("Custom Agent");
expect(firstUserHeader).toEqual("VALUE");
expect(secondUserHeader).toEqual("VALUE");
expect(deleteUserHeader).toEqual("VALUE");
});
});
}); });
function makeClosedPromise(transport: LongPollingTransport): Promise<void> { function makeClosedPromise(transport: LongPollingTransport): Promise<void> {

View File

@ -1,6 +1,7 @@
// 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 { MessageHeaders } from "../src/IHubProtocol";
import { TransferFormat } from "../src/ITransport"; import { TransferFormat } from "../src/ITransport";
import { HttpClient, HttpRequest } from "../src/HttpClient"; import { HttpClient, HttpRequest } from "../src/HttpClient";
@ -17,7 +18,7 @@ registerUnhandledRejectionHandler();
describe("ServerSentEventsTransport", () => { describe("ServerSentEventsTransport", () => {
it("does not allow non-text formats", async () => { it("does not allow non-text formats", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true); const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
await expect(sse.connect("", TransferFormat.Binary)) await expect(sse.connect("", TransferFormat.Binary))
.rejects .rejects
@ -27,7 +28,7 @@ describe("ServerSentEventsTransport", () => {
it("connect waits for EventSource to be connected", async () => { it("connect waits for EventSource to be connected", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true); const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
let connectComplete: boolean = false; let connectComplete: boolean = false;
const connectPromise = (async () => { const connectPromise = (async () => {
@ -48,7 +49,7 @@ describe("ServerSentEventsTransport", () => {
it("connect failure does not call onclose handler", async () => { it("connect failure does not call onclose handler", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true); const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
let closeCalled = false; let closeCalled = false;
sse.onclose = () => closeCalled = true; sse.onclose = () => closeCalled = true;
@ -169,7 +170,7 @@ describe("ServerSentEventsTransport", () => {
it("send throws if not connected", async () => { it("send throws if not connected", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true); const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
await expect(sse.send("")) await expect(sse.send(""))
.rejects .rejects
@ -221,10 +222,31 @@ describe("ServerSentEventsTransport", () => {
expect(request!.url).toBe("http://example.com"); expect(request!.url).toBe("http://example.com");
}); });
}); });
it("overwrites library headers with user headers", async () => {
await VerifyLogger.run(async (logger) => {
let request: HttpRequest;
const httpClient = new TestHttpClient().on((r) => {
request = r;
return "";
});
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
const sse = await createAndStartSSE(logger, "http://example.com", undefined, httpClient, headers);
expect((TestEventSource.eventSource.eventSourceInitDict as any).headers["User-Agent"]).toEqual("Custom Agent");
expect((TestEventSource.eventSource.eventSourceInitDict as any).headers["X-HEADER"]).toEqual("VALUE");
await sse.send("");
expect(request!.headers!["User-Agent"]).toEqual("Custom Agent");
expect(request!.headers!["X-HEADER"]).toEqual("VALUE");
expect(request!.url).toBe("http://example.com");
});
});
}); });
async function createAndStartSSE(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), httpClient?: HttpClient): Promise<ServerSentEventsTransport> { async function createAndStartSSE(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), httpClient?: HttpClient, headers?: MessageHeaders): Promise<ServerSentEventsTransport> {
const sse = new ServerSentEventsTransport(httpClient || new TestHttpClient(), accessTokenFactory, logger, true, TestEventSource, true); const sse = new ServerSentEventsTransport(httpClient || new TestHttpClient(), accessTokenFactory, logger, true, TestEventSource, true, headers || {});
const connectPromise = sse.connect(url || "http://example.com", TransferFormat.Text); const connectPromise = sse.connect(url || "http://example.com", TransferFormat.Text);
await TestEventSource.eventSource.openSet; await TestEventSource.eventSource.openSet;

View File

@ -1,6 +1,7 @@
// 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 { MessageHeaders } from "../src/IHubProtocol";
import { ILogger } from "../src/ILogger"; import { ILogger } from "../src/ILogger";
import { TransferFormat } from "../src/ITransport"; import { TransferFormat } from "../src/ITransport";
import { getUserAgentHeader } from "../src/Utils"; import { getUserAgentHeader } from "../src/Utils";
@ -24,7 +25,7 @@ describe("WebSocketTransport", () => {
it("connect waits for WebSocket to be connected", async () => { it("connect waits for WebSocket to be connected", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket); const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
let connectComplete: boolean = false; let connectComplete: boolean = false;
const connectPromise = (async () => { const connectPromise = (async () => {
@ -46,7 +47,7 @@ describe("WebSocketTransport", () => {
it("connect fails if there is error during connect", async () => { it("connect fails if there is error during connect", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestErrorEvent; (global as any).ErrorEvent = TestErrorEvent;
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket); const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
let connectComplete: boolean = false; let connectComplete: boolean = false;
const connectPromise = (async () => { const connectPromise = (async () => {
@ -70,7 +71,7 @@ describe("WebSocketTransport", () => {
it("connect failure does not call onclose handler", async () => { it("connect failure does not call onclose handler", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestErrorEvent; (global as any).ErrorEvent = TestErrorEvent;
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket); const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
let closeCalled = false; let closeCalled = false;
webSocket.onclose = () => closeCalled = true; webSocket.onclose = () => closeCalled = true;
@ -257,6 +258,33 @@ describe("WebSocketTransport", () => {
}); });
}); });
it("overwrites library headers with user headers", async () => {
await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestEvent;
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
const webSocket = await createAndStartWebSocket(logger, undefined, undefined, undefined, headers);
let closeCalled: boolean = false;
let error: Error;
webSocket.onclose = (e) => {
closeCalled = true;
error = e!;
};
expect(TestWebSocket.webSocket.options!.headers[`User-Agent`]).toEqual("Custom Agent");
expect(TestWebSocket.webSocket.options!.headers[`X-HEADER`]).toEqual("VALUE");
await webSocket.stop();
expect(closeCalled).toBe(true);
expect(error!).toBeUndefined();
await expect(webSocket.send(""))
.rejects
.toBe("WebSocket is not in the OPEN state");
});
});
it("is closed from 'onreceive' callback throwing", async () => { it("is closed from 'onreceive' callback throwing", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestEvent; (global as any).ErrorEvent = TestEvent;
@ -270,7 +298,7 @@ describe("WebSocketTransport", () => {
}; };
const receiveError = new Error("callback error"); const receiveError = new Error("callback error");
webSocket.onreceive = (data) => { webSocket.onreceive = () => {
throw receiveError; throw receiveError;
}; };
@ -290,7 +318,7 @@ describe("WebSocketTransport", () => {
it("does not run onclose callback if Transport does not fully connect and exits", async () => { it("does not run onclose callback if Transport does not fully connect and exits", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestErrorEvent; (global as any).ErrorEvent = TestErrorEvent;
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket); const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
const connectPromise = webSocket.connect("http://example.com", TransferFormat.Text); const connectPromise = webSocket.connect("http://example.com", TransferFormat.Text);
@ -318,8 +346,8 @@ describe("WebSocketTransport", () => {
}); });
}); });
async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), format?: TransferFormat): Promise<WebSocketTransport> { async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), format?: TransferFormat, headers?: MessageHeaders): Promise<WebSocketTransport> {
const webSocket = new WebSocketTransport(new TestHttpClient(), accessTokenFactory, logger, true, TestWebSocket); const webSocket = new WebSocketTransport(new TestHttpClient(), accessTokenFactory, logger, true, TestWebSocket, headers || {});
const connectPromise = webSocket.connect(url || "http://example.com", format || TransferFormat.Text); const connectPromise = webSocket.connect(url || "http://example.com", format || TransferFormat.Text);