Add timeout option for keycloak-admin-client

Closes #42644

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc
2025-12-15 16:46:09 +01:00
committed by Marek Posolda
parent 94ee6d81fb
commit 40eb51f10c
3 changed files with 67 additions and 3 deletions

View File

@@ -19,6 +19,8 @@ import { WhoAmI } from "./resources/whoAmI.js";
import { Credentials, getToken } from "./utils/auth.js";
import { defaultBaseUrl, defaultRealm } from "./utils/constants.js";
export type RequestOptions = Omit<RequestInit, "signal">;
export interface TokenProvider {
getAccessToken: () => Promise<string | undefined>;
}
@@ -26,8 +28,9 @@ export interface TokenProvider {
export interface ConnectionConfig {
baseUrl?: string;
realmName?: string;
requestOptions?: RequestInit;
requestOptions?: RequestOptions;
requestArgOptions?: Pick<RequestArgs, "catchNotFound">;
timeout?: number;
}
export class KeycloakAdminClient {
@@ -56,14 +59,16 @@ export class KeycloakAdminClient {
public scope?: string;
public accessToken?: string;
public refreshToken?: string;
public timeout?: number;
#requestOptions?: RequestInit;
#requestOptions?: RequestOptions;
#globalRequestArgOptions?: Pick<RequestArgs, "catchNotFound">;
#tokenProvider?: TokenProvider;
constructor(connectionConfig?: ConnectionConfig) {
this.baseUrl = connectionConfig?.baseUrl || defaultBaseUrl;
this.realmName = connectionConfig?.realmName || defaultRealm;
this.timeout = connectionConfig?.timeout;
this.#requestOptions = connectionConfig?.requestOptions;
this.#globalRequestArgOptions = connectionConfig?.requestArgOptions;
@@ -93,7 +98,10 @@ export class KeycloakAdminClient {
realmName: this.realmName,
scope: this.scope,
credentials,
requestOptions: this.#requestOptions,
requestOptions: {
...this.#requestOptions,
...(this.timeout ? { signal: AbortSignal.timeout(this.timeout) } : {}),
},
});
this.accessToken = accessToken;
this.refreshToken = refreshToken;

View File

@@ -248,6 +248,9 @@ export class Agent {
...requestOptions,
headers: requestHeaders,
method,
...(this.#client.timeout
? { signal: AbortSignal.timeout(this.#client.timeout) }
: {}),
});
// now we get the response of the http request

View File

@@ -0,0 +1,53 @@
import { expect } from "chai";
import { KeycloakAdminClient } from "../src/client.js";
import { credentials } from "./constants.js";
import { Server, createServer } from "node:http";
describe("Timeout", () => {
let server: Server;
before(async () => {
server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
setTimeout(() => res.end("Hello, world!\n"), 1500);
});
server.listen(8888, "localhost");
});
after(async () => {
await server[Symbol.asyncDispose]();
});
void it("create without timeout", async () => {
const client = new KeycloakAdminClient({
baseUrl: "http://localhost:8888",
});
try {
await client.auth(credentials);
} catch (error) {
expect(error).to.be.an("Error");
expect((error as Error).message).to.contain("Unexpected token 'H'");
return;
}
expect.fail(null, null, "auth did not fail");
});
void it("create with timeout", async () => {
const client = new KeycloakAdminClient({
baseUrl: "http://localhost:8888",
timeout: 1000,
});
try {
await client.auth(credentials);
} catch (error) {
expect(error).to.be.an("DOMException");
expect((error as DOMException).message).to.contain(
"The operation was aborted due to timeout",
);
return;
}
expect.fail(null, null, "auth did not fail");
});
});