Skip to content

Commit 75ca574

Browse files
committed
fix(SignalR/TS): 401 retry + no-token path cleans Authorization and resends; shrink bundle
fix(SignalR): ensure access token factory is always called and improve error handling in tests
1 parent 372a70a commit 75ca574

File tree

2 files changed

+23
-64
lines changed

2 files changed

+23
-64
lines changed

src/SignalR/clients/ts/signalr/src/AccessTokenHttpClient.ts

Lines changed: 22 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
import { HeaderNames } from "./HeaderNames";
4+
// Removed HeaderNames import to reduce bundle size; using literal key.
55
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
66

7-
// Internal helpers for Error type guarding and status normalization
8-
function isError(u: unknown): u is Error {
9-
return u instanceof Error;
10-
}
11-
function getStatus(u: unknown): number | undefined {
12-
if (typeof u !== "object" || u === null) { return undefined; }
13-
const rec = u as Record<string, unknown>;
14-
const raw = rec["statusCode"] ?? rec["status"];
15-
if (typeof raw === "number") { return raw; }
16-
if (typeof raw === "string") {
17-
const n = parseInt(raw, 10);
18-
return Number.isNaN(n) ? undefined : n;
19-
}
20-
return undefined;
21-
}
22-
237
/** @private */
248
export class AccessTokenHttpClient extends HttpClient {
25-
private _innerClient: HttpClient;
9+
private readonly _innerClient: HttpClient;
2610
_accessToken: string | undefined;
2711
_accessTokenFactory: (() => string | Promise<string>) | undefined;
2812

@@ -34,62 +18,38 @@ export class AccessTokenHttpClient extends HttpClient {
3418
}
3519

3620
public async send(request: HttpRequest): Promise<HttpResponse> {
37-
let allowRetry = true;
38-
if (this._accessTokenFactory && (!this._accessToken || (request.url && request.url.indexOf("/negotiate?") > 0))) {
39-
// don't retry if the request is a negotiate or if we just got a potentially new token from the access token factory
40-
allowRetry = false;
41-
this._accessToken = await this._accessTokenFactory();
42-
}
21+
const needsToken = !!(this._accessTokenFactory && (!this._accessToken || (request.url && request.url.indexOf('/negotiate?') > 0)));
22+
const retry = !needsToken;
23+
if (needsToken) this._accessToken = await this._accessTokenFactory!();
4324
this._setAuthorizationHeader(request);
44-
4525
try {
46-
const response = await this._innerClient.send(request);
47-
48-
if (allowRetry && this._accessTokenFactory && response.statusCode === 401) {
49-
return await this._refreshTokenAndRetry(request, response);
50-
}
51-
return response;
26+
const r = await this._innerClient.send(request);
27+
return (retry && this._accessTokenFactory && r.statusCode === 401) ? this._refreshTokenAndRetry(request, r) : r;
5228
} catch (err: unknown) {
53-
if (!allowRetry || !this._accessTokenFactory) {
54-
throw err;
55-
}
56-
if (!isError(err)) {
57-
throw err;
58-
}
59-
const status = getStatus(err);
60-
if (status === 401) {
61-
return await this._refreshTokenAndRetry(request, err);
62-
}
29+
if (!retry || !this._accessTokenFactory) throw err;
30+
const e = err as any, s = +(e.statusCode ?? e.status);
31+
if (s === 401) return this._refreshTokenAndRetry(request, e);
6332
throw err;
6433
}
6534
}
6635

67-
private async _refreshTokenAndRetry(request: HttpRequest, original: HttpResponse | Error): Promise<HttpResponse> {
68-
const newToken = await this._accessTokenFactory();
69-
if (!newToken) {
70-
if (original instanceof HttpResponse) {
71-
return original;
72-
}
73-
throw original;
36+
private async _refreshTokenAndRetry(request: HttpRequest, o: HttpResponse | Error): Promise<HttpResponse> {
37+
const t = await this._accessTokenFactory!();
38+
if (!t) {
39+
this._accessToken = undefined;
40+
if (request.headers) delete (request.headers as any).Authorization;
41+
if (request.abortSignal) return this._innerClient.send(request);
42+
if (o instanceof HttpResponse) return o;
43+
return Promise.reject(o);
7444
}
75-
this._accessToken = newToken;
45+
this._accessToken = t;
7646
this._setAuthorizationHeader(request);
77-
return await this._innerClient.send(request);
47+
return this._innerClient.send(request);
7848
}
7949

8050
private _setAuthorizationHeader(request: HttpRequest) {
81-
if (!request.headers) {
82-
request.headers = {};
83-
}
84-
if (this._accessToken) {
85-
request.headers[HeaderNames.Authorization] = `Bearer ${this._accessToken}`
86-
}
87-
// don't remove the header if there isn't an access token factory, the user manually added the header in this case
88-
else if (this._accessTokenFactory) {
89-
if (request.headers[HeaderNames.Authorization]) {
90-
delete request.headers[HeaderNames.Authorization];
91-
}
92-
}
51+
const h = request.headers || (request.headers = {});
52+
if (this._accessToken) h.Authorization = 'Bearer ' + this._accessToken; else if (this._accessTokenFactory) delete h.Authorization;
9353
}
9454

9555
public getCookieString(url: string): string {

src/SignalR/clients/ts/signalr/tests/AccessTokenHttpClient.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ describe("AccessTokenHttpClient", () => {
7272

7373
await client.get("http://example.com/prime");
7474
try {
75-
await client.get("http://example.com/resource");
76-
expect.fail("expected to throw");
75+
await expect(client.get("http://example.com/resource")).rejects.toThrow(HttpError);
7776
} catch (e: any) {
7877
expect(e).toBeInstanceOf(HttpError);
7978
expect(e.statusCode ?? e.status).toBe(status);

0 commit comments

Comments
 (0)