Skip to main content
Glama
base-client.ts8.59 kB
/** * Base HTTP-klient för alla Skolverkets API:er * Med caching, rate limiting och förbättrad felhantering */ import axios, { AxiosInstance, AxiosError } from 'axios'; import axiosRetry from 'axios-retry'; import pLimit from 'p-limit'; import { v4 as uuidv4 } from 'uuid'; import { log, createRequestLogger } from '../logger.js'; import { cache } from '../cache.js'; import { SkolverketApiError, RateLimitError, AuthenticationError, TransientError } from '../errors.js'; // Constants for default values const DEFAULT_TIMEOUT_MS = 30000; // 30 seconds const DEFAULT_MAX_RETRIES = 3; const DEFAULT_RETRY_DELAY_MS = 1000; // 1 second const DEFAULT_MAX_CONCURRENT = 5; const DEFAULT_CACHE_TTL_MS = 3600000; // 1 hour const MCP_VERSION = '2.1.3'; // Sensitive headers to redact in logs const SENSITIVE_HEADERS = ['authorization', 'api-key', 'x-api-key', 'apikey']; export interface BaseClientConfig { baseURL: string; timeout?: number; userAgent?: string; maxConcurrent?: number; // Max antal samtidiga requests maxRetries?: number; // Max antal retries (default: 3) retryDelay?: number; // Base delay mellan retries i ms (default: 1000) apiKey?: string; // API-nyckel om krävs authHeader?: string; // Namn på auth header (default: 'Authorization') customAcceptHeader?: string; // Custom Accept header för specifika API:er } export class BaseApiClient { protected client: AxiosInstance; private limiter: ReturnType<typeof pLimit>; /** * Redact sensitive headers for logging */ private redactHeaders(headers: any): any { if (!headers || typeof headers !== 'object') return headers; const redacted = { ...headers }; for (const key of Object.keys(redacted)) { if (SENSITIVE_HEADERS.includes(key.toLowerCase())) { redacted[key] = '***REDACTED***'; } } return redacted; } constructor(config: BaseClientConfig) { const headers: Record<string, string> = { 'Accept': config.customAcceptHeader || 'application/json', 'User-Agent': config.userAgent || `skolverket-mcp/${MCP_VERSION}` }; // Lägg till API-nyckel om angiven if (config.apiKey) { const authHeaderName = config.authHeader || 'Authorization'; headers[authHeaderName] = `Bearer ${config.apiKey}`; } this.client = axios.create({ baseURL: config.baseURL, timeout: config.timeout || DEFAULT_TIMEOUT_MS, headers }); // Konfigurera axios-retry med exponentiell backoff axiosRetry(this.client, { retries: config.maxRetries || DEFAULT_MAX_RETRIES, retryDelay: (retryCount) => { const baseDelay = config.retryDelay || DEFAULT_RETRY_DELAY_MS; return baseDelay * Math.pow(2, retryCount - 1); // Exponentiell backoff }, retryCondition: (error: AxiosError) => { // Retry på nätverksfel eller 5xx errors if (!error.response) return true; // Nätverksfel const status = error.response.status; // Retry på 429 (rate limit), 500, 502, 503, 504 return status === 429 || (status >= 500 && status <= 504); }, onRetry: (retryCount, error, requestConfig) => { log.warn(`Retrying request (attempt ${retryCount})`, { url: requestConfig.url, method: requestConfig.method, error: error.message }); } }); // Rate limiter - konfigurerbar via env eller config this.limiter = pLimit(config.maxConcurrent || DEFAULT_MAX_CONCURRENT); // Lägg till request interceptor för logging this.client.interceptors.request.use( (config) => { log.debug('API Request', { method: config.method?.toUpperCase(), baseURL: config.baseURL, url: config.url, fullURL: `${config.baseURL}${config.url}`, params: config.params, headers: this.redactHeaders(config.headers) }); return config; }, (error) => { log.error('API Request Error', { error: error.message }); return Promise.reject(error); } ); // Lägg till response interceptor för error handling this.client.interceptors.response.use( (response) => { log.debug('API Response', { status: response.status, url: response.config.url }); return response; }, (error: AxiosError) => { return this.handleError(error); } ); } private handleError(error: AxiosError): never { const requestId = uuidv4(); const reqLog = createRequestLogger(requestId); const url = error.config?.url || 'unknown'; const timestamp = new Date().toISOString(); if (error.response) { const status = error.response.status; const data = error.response.data; reqLog.error('API Error Response', { status, url, data, headers: error.response.headers }); // Autentiseringsfel if (status === 401 || status === 403) { throw new AuthenticationError( status === 401 ? 'API authentication failed. Check if API key is required and valid.' : 'Access forbidden. Check API permissions.', url ); } // Rate limiting if (status === 429) { const retryAfter = error.response.headers['retry-after']; throw new RateLimitError( 'API rate limit reached. Please retry after the specified time.', retryAfter ? parseInt(retryAfter) : undefined ); } // Tillfälliga fel (5xx) if (status >= 500 && status <= 504) { throw new TransientError( 'Temporary server error. The request can be retried.', status, url ); } // Övriga API-fel const errorMessage = this.formatErrorMessage(status, data); throw new SkolverketApiError( errorMessage, status, data, 'API_ERROR', url, (error.config as any)?.['axios-retry']?.retryCount || 1, timestamp ); } else if (error.request) { reqLog.error('API Network Error', { message: error.message, url, code: error.code }); throw new TransientError( 'Could not reach the API. Check your internet connection.', undefined, url ); } else { reqLog.error('API Request Setup Error', { message: error.message, url }); throw new SkolverketApiError( `Request configuration error: ${error.message}`, undefined, undefined, 'CONFIG_ERROR', url, 1, timestamp ); } } private formatErrorMessage(status: number, data: any): string { if (typeof data === 'object' && data !== null) { if (data.detail) { return `Skolverket API error (${status}): ${data.detail}`; } if (data.message) { return `Skolverket API error (${status}): ${data.message}`; } } return `Skolverket API error: ${status} - ${JSON.stringify(data)}`; } /** * GET-request med rate limiting */ protected async get<T>(url: string, params?: any, options?: { headers?: Record<string, string> }): Promise<T> { return this.limiter(async () => { const response = await this.client.get<T>(url, { params, headers: options?.headers }); return response.data; }); } /** * GET-request med caching och rate limiting */ protected async getCached<T>( url: string, params?: any, ttl: number = DEFAULT_CACHE_TTL_MS ): Promise<T> { // Skapa cache key från URL och params const cacheKey = `${url}:${JSON.stringify(params || {})}`; return cache.getOrFetch( cacheKey, () => this.get<T>(url, params), ttl ); } /** * POST-request med rate limiting */ protected async post<T>(url: string, data?: any): Promise<T> { return this.limiter(async () => { const response = await this.client.post<T>(url, data); return response.data; }); } /** * PUT-request med rate limiting */ protected async put<T>(url: string, data?: any): Promise<T> { return this.limiter(async () => { const response = await this.client.put<T>(url, data); return response.data; }); } /** * DELETE-request med rate limiting */ protected async delete<T>(url: string): Promise<T> { return this.limiter(async () => { const response = await this.client.delete<T>(url); return response.data; }); } }

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/isakskogstad/skolverket-syllabus-mcp'

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