Skip to main content
Glama
RequestManager.ts7.43 kB
/** * HTTP Request Manager * Handles all HTTP operations, rate limiting, and retries */ // Use native fetch in Node.js 18+ import type { HTTPMethod, RequestOptions, ClientStats, WordPressClientConfig } from "@/types/client.js"; import { WordPressAPIError, RateLimitError } from "@/types/client.js"; import { config } from "@/config/Config.js"; import { BaseManager } from "./BaseManager.js"; import { AuthenticationManager } from "./AuthenticationManager.js"; import { debug, startTimer } from "@/utils/debug.js"; import { getUserAgent } from "@/utils/version.js"; export class RequestManager extends BaseManager { private stats: ClientStats; private lastRequestTime: number = 0; private requestInterval: number; private authManager: AuthenticationManager; constructor(clientConfig: WordPressClientConfig, authManager: AuthenticationManager) { super(clientConfig); this.authManager = authManager; this.requestInterval = 60000 / config().security.rateLimit; this.stats = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, rateLimitHits: 0, authFailures: 0, errors: 0, }; } /** * Make HTTP request with retry logic and rate limiting */ async request<T>(method: HTTPMethod, endpoint: string, data?: unknown, options: RequestOptions = {}): Promise<T> { const timer = startTimer(); try { await this.enforceRateLimit(); const response = await this.makeRequestWithRetry(method, endpoint, data, options); this.stats.successfulRequests++; this.updateAverageResponseTime(timer.end()); return response as T; } catch (_error) { this.stats.failedRequests++; this.handleError(_error, `${method} ${endpoint}`); } finally { this.stats.totalRequests++; } } /** * Make request with retry logic */ private async makeRequestWithRetry<T>( method: HTTPMethod, endpoint: string, data?: unknown, options: RequestOptions = {}, ): Promise<T> { let lastError: unknown; const maxRetries = options.retries ?? this.config.maxRetries ?? 3; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await this.makeRequest<T>(method, endpoint, data, options); } catch (error: unknown) { lastError = error; // Type guard for error-like objects const isErrorLike = (err: unknown): err is { statusCode?: number; message?: string } => { return typeof err === "object" && err !== null; }; // Don't retry on authentication errors or client errors if ((isErrorLike(error) && error.statusCode && error.statusCode < 500) || attempt === maxRetries) { throw error; } const errorMessage = isErrorLike(error) && error.message ? error.message : String(error); debug.log(`Request failed (attempt ${attempt}/${maxRetries}):`, errorMessage); // Exponential backoff const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError; } /** * Make single HTTP request */ private async makeRequest<T>( method: HTTPMethod, endpoint: string, data?: unknown, options: RequestOptions = {}, ): Promise<T> { const url = this.buildUrl(endpoint); const timeout = options.timeout ?? this.config.timeout ?? 30000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { // Get authentication headers const authHeaders = await this.authManager.getAuthHeaders(); const fetchOptions: RequestInit = { method, headers: { "Content-Type": "application/json", "User-Agent": getUserAgent(), ...authHeaders, // Add auth headers before custom headers ...options.headers, }, signal: controller.signal, }; if (data && method !== "GET") { if ( data instanceof FormData || (typeof data === "object" && "append" in data && typeof (data as Record<string, unknown>).append === "function") ) { // For FormData, don't set Content-Type (let fetch set it with boundary) const headers = fetchOptions.headers as Record<string, string>; if ("Content-Type" in headers) { delete headers["Content-Type"]; } fetchOptions.body = data as FormData; } else if (Buffer.isBuffer(data)) { // For Buffer data, keep Content-Type from headers fetchOptions.body = data; } else if (typeof data === "string") { fetchOptions.body = data; } else { fetchOptions.body = JSON.stringify(data); } } debug.log(`API Request: ${method} ${url}`); const response = await fetch(url, fetchOptions); if (!response.ok) { await this.handleErrorResponse(response); } const responseData = await response.json(); return responseData as T; } finally { clearTimeout(timeoutId); } } /** * Handle HTTP error responses */ private async handleErrorResponse(response: Response): Promise<never> { let errorData: Record<string, unknown> = {}; try { const jsonData = await response.json(); if (typeof jsonData === "object" && jsonData !== null) { errorData = jsonData as Record<string, unknown>; } } catch { // Ignore JSON parsing errors } const message = (typeof errorData.message === "string" ? errorData.message : undefined) || `HTTP ${response.status}: ${response.statusText}`; const code = (typeof errorData.code === "string" ? errorData.code : undefined) || "http_error"; if (response.status === 429) { this.stats.rateLimitHits++; throw new RateLimitError(message, Date.now() + 60000); // Retry after 1 minute } if (response.status === 401 || response.status === 403) { this.stats.authFailures++; } throw new WordPressAPIError(message, response.status, code, errorData); } /** * Build full URL from endpoint */ private buildUrl(endpoint: string): string { const baseUrl = this.config.baseUrl.replace(/\/$/, ""); const apiBase = "/wp-json/wp/v2"; const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; return `${baseUrl}${apiBase}${cleanEndpoint}`; } /** * Enforce rate limiting */ private async enforceRateLimit(): Promise<void> { const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.requestInterval) { const delay = this.requestInterval - timeSinceLastRequest; await new Promise((resolve) => setTimeout(resolve, delay)); } this.lastRequestTime = Date.now(); } /** * Update average response time */ private updateAverageResponseTime(responseTime: number): void { const totalRequests = this.stats.successfulRequests; const currentAverage = this.stats.averageResponseTime; this.stats.averageResponseTime = (currentAverage * (totalRequests - 1) + responseTime) / totalRequests; } /** * Get request statistics */ getStats(): ClientStats { return { ...this.stats }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server