Skip to main content
Glama
http-client.ts6.62 kB
/** * HTTP REST API Client for Integration Tests * * Provides a simple HTTP client for testing REST API endpoints. * Handles request/response formatting and error handling. */ import { IncomingMessage } from 'http'; import * as http from 'http'; import * as https from 'https'; import { URL } from 'url'; export interface RestApiResponse { success: boolean; data?: any; error?: { code: string; message: string; details?: any; }; meta?: { timestamp: string; requestId?: string; version: string; }; } export interface HttpClientOptions { baseUrl?: string; timeout?: number; headers?: Record<string, string>; } export class HttpRestApiClient { private baseUrl: string; private timeout: number; private defaultHeaders: Record<string, string>; constructor(options: HttpClientOptions = {}) { // Use MCP_BASE_URL from environment if set (for in-cluster testing), otherwise default to localhost this.baseUrl = options.baseUrl || process.env.MCP_BASE_URL || 'http://localhost:3456'; this.timeout = options.timeout || 1800000; // Default to 30 minutes for integration tests (slower AI providers) this.defaultHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json', ...options.headers, }; } /** * Make GET request to API endpoint */ async get(path: string, headers?: Record<string, string>): Promise<RestApiResponse> { return this.request('GET', path, undefined, headers); } /** * Make POST request to API endpoint */ async post(path: string, body?: any, headers?: Record<string, string>): Promise<RestApiResponse> { return this.request('POST', path, body, headers); } /** * Make PUT request to API endpoint */ async put(path: string, body?: any, headers?: Record<string, string>): Promise<RestApiResponse> { return this.request('PUT', path, body, headers); } /** * Make DELETE request to API endpoint */ async delete(path: string, headers?: Record<string, string>): Promise<RestApiResponse> { return this.request('DELETE', path, undefined, headers); } /** * Make generic HTTP request */ private async request( method: string, path: string, body?: any, headers?: Record<string, string> ): Promise<RestApiResponse> { const url = new URL(path, this.baseUrl); const requestHeaders = { ...this.defaultHeaders, ...headers }; const requestBody = body ? JSON.stringify(body) : undefined; if (requestBody) { requestHeaders['Content-Length'] = Buffer.byteLength(requestBody).toString(); } const options = { method, headers: requestHeaders, }; return new Promise((resolve, reject) => { const client = url.protocol === 'https:' ? https : http; const startTime = Date.now(); let socketAssigned = false; let socketTimeoutHandler: (() => void) | null = null; let currentSocket: any = null; const cleanup = () => { // Remove socket timeout listener to prevent memory leak on reused sockets if (currentSocket && socketTimeoutHandler) { currentSocket.removeListener('timeout', socketTimeoutHandler); currentSocket.setTimeout(0); // Disable socket timeout } }; const req = client.request(url, options, (res: IncomingMessage) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { cleanup(); try { const response = this.parseResponse(data, res.statusCode || 500); resolve(response); } catch (error) { reject(new Error(`Failed to parse response: ${error}`)); } }); }); req.once('socket', (socket) => { socketAssigned = true; currentSocket = socket; // Set timeout on socket directly for more reliable timeout handling socket.setTimeout(this.timeout); socketTimeoutHandler = () => { const elapsed = Date.now() - startTime; cleanup(); req.destroy(); reject(new Error(`Socket timeout after ${elapsed}ms (configured: ${this.timeout}ms, socket assigned: true)`)); }; socket.once('timeout', socketTimeoutHandler); }); req.setTimeout(this.timeout); req.on('error', (error) => { const elapsed = Date.now() - startTime; cleanup(); reject(new Error(`Request failed after ${elapsed}ms: ${error.message} (socket assigned: ${socketAssigned})`)); }); req.on('timeout', () => { const elapsed = Date.now() - startTime; cleanup(); req.destroy(); reject(new Error(`Request timeout after ${elapsed}ms (configured: ${this.timeout}ms, socket assigned: ${socketAssigned})`)); }); if (requestBody) { req.write(requestBody); } req.end(); }); } /** * Parse HTTP response into standard format */ private parseResponse(data: string, statusCode: number): RestApiResponse { // Handle empty responses if (!data.trim()) { return { success: statusCode >= 200 && statusCode < 300, error: statusCode >= 400 ? { code: 'EMPTY_RESPONSE', message: `HTTP ${statusCode}: Empty response`, } : undefined, }; } try { const parsed = JSON.parse(data); // If already in REST API format, return as-is if (typeof parsed === 'object' && 'success' in parsed) { return parsed; } // Wrap raw response in REST API format return { success: statusCode >= 200 && statusCode < 300, data: parsed, error: statusCode >= 400 ? { code: 'HTTP_ERROR', message: `HTTP ${statusCode}`, details: parsed, } : undefined, }; } catch (error) { // Handle non-JSON responses return { success: statusCode >= 200 && statusCode < 300, data: statusCode < 400 ? data : undefined, error: statusCode >= 400 ? { code: 'INVALID_JSON', message: `HTTP ${statusCode}: ${data}`, } : undefined, }; } } /** * Set base URL for all requests */ setBaseUrl(baseUrl: string): void { this.baseUrl = baseUrl; } /** * Set timeout for all requests */ setTimeout(timeout: number): void { this.timeout = timeout; } /** * Add default header for all requests */ setDefaultHeader(name: string, value: string): void { this.defaultHeaders[name] = value; } }

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/vfarcic/dot-ai'

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