feat: add BaseClient with shared Axios instance and interceptors
This commit is contained in:
77
src/clients/base-client.ts
Normal file
77
src/clients/base-client.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// src/clients/base-client.ts
|
||||||
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||||
|
import { TokenConfigLoader, TokenConfig } from '../config.js';
|
||||||
|
|
||||||
|
export interface ClientOptions {
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseClient {
|
||||||
|
protected axiosInstance!: AxiosInstance;
|
||||||
|
private tokenSource: string | null = null;
|
||||||
|
private _initialized = false;
|
||||||
|
private _initPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
constructor(options: ClientOptions = {}) {
|
||||||
|
this._initPromise = this._init(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _init(options: ClientOptions): Promise<void> {
|
||||||
|
try {
|
||||||
|
const config = await TokenConfigLoader.load();
|
||||||
|
this.tokenSource = config.source;
|
||||||
|
this.axiosInstance = axios.create({
|
||||||
|
baseURL: 'https://api.bitbucket.org/2.0',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
auth: { username: config.email, password: config.token },
|
||||||
|
timeout: options.timeout || 30000,
|
||||||
|
maxRedirects: 5,
|
||||||
|
});
|
||||||
|
this.axiosInstance.interceptors.request.use(cfg => {
|
||||||
|
cfg.headers['User-Agent'] = 'Bitbucket-MCP-Server/1.0.0';
|
||||||
|
return cfg;
|
||||||
|
});
|
||||||
|
this.axiosInstance.interceptors.response.use(
|
||||||
|
r => r,
|
||||||
|
(error: AxiosError) => this._handleResponseError(error),
|
||||||
|
);
|
||||||
|
this._initialized = true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Unable to initialize Bitbucket client: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async ensureInitialized(): Promise<void> {
|
||||||
|
if (this._initialized) return;
|
||||||
|
await this._initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleResponseError(error: AxiosError): Promise<never> {
|
||||||
|
const status = error.response?.status;
|
||||||
|
if (status === 401) {
|
||||||
|
console.error('🔐 Bitbucket API Authentication Error (401)');
|
||||||
|
console.error(` Token source: ${this.tokenSource || 'unknown'}`);
|
||||||
|
console.error(` URL: ${error.config?.url}`);
|
||||||
|
} else if (status === 403) {
|
||||||
|
console.error('🚫 Bitbucket API Forbidden (403)');
|
||||||
|
console.error(` URL: ${error.config?.url}`);
|
||||||
|
} else if (status === 429) {
|
||||||
|
const retryAfter = error.response?.headers['retry-after'];
|
||||||
|
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;
|
||||||
|
console.log(`Rate limited. Retrying in ${delay}ms...`);
|
||||||
|
await this._sleep(delay);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected formatError(error: any): string {
|
||||||
|
if (error?.response?.status) {
|
||||||
|
return `HTTP ${error.response.status}: ${error.message}`;
|
||||||
|
}
|
||||||
|
return error?.message || 'Unknown error';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user