Notion MCP Server
by tkc
Verified
- notion-mcp
- src
- notion
- utils
/**
* Base API client with error handling, retries, and rate limiting
*/
import { logger } from './logger';
import { rateLimiter } from './rate-limiter';
export interface ApiRequestOptions {
method: string;
path: string;
body?: any;
query?: Record<string, string>;
maxRetries?: number;
retryDelay?: number;
}
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
status?: number;
};
}
export class ApiClient {
private baseUrl: string;
private headers: Record<string, string>;
private timeout: number;
constructor(
baseUrl: string,
headers: Record<string, string>,
timeout: number = 30000, // Default timeout value
) {
this.baseUrl = baseUrl;
this.headers = headers;
this.timeout = timeout;
}
/**
* Send API request with retry and rate limiting
*/
public async request<T = any>(
options: ApiRequestOptions,
): Promise<ApiResponse<T>> {
const {
method,
path,
body,
query,
maxRetries = 3,
retryDelay = 1000,
} = options;
let attempts = 0;
let lastError: Error | null = null;
while (attempts <= maxRetries) {
try {
// Wait for rate limiter
await rateLimiter.acquire();
// Build URL with query parameters
let url = `${this.baseUrl}${path}`;
if (query && Object.keys(query).length > 0) {
const params = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.append(key, value);
}
});
url = `${url}?${params.toString()}`;
}
// Prepare request
const requestOptions: RequestInit = {
method,
headers: this.headers,
signal: AbortSignal.timeout(this.timeout),
};
if (body) {
requestOptions.body = JSON.stringify(body);
}
// Log request (without sensitive data)
logger.debug('API request', {
method,
path,
hasBody: !!body,
attempt: attempts + 1,
});
// Send request
const startTime = Date.now();
const response = await fetch(url, requestOptions);
const requestTime = Date.now() - startTime;
// Parse response
const responseData = await response.json();
// Log response time
logger.debug('API response received', {
method,
path,
status: response.status,
time: `${requestTime}ms`,
});
// Handle error responses
if (!response.ok) {
const error = new Error(
`API Error: ${response.status} ${responseData.message || response.statusText}`,
);
Object.assign(error, { status: response.status, data: responseData });
throw error;
}
// Return successful response
return {
success: true,
data: responseData as T,
};
} catch (error) {
lastError = error as Error;
attempts += 1;
// Log the error
logger.warn(
`API request failed (attempt ${attempts}/${maxRetries + 1})`,
{
method,
path,
error: error instanceof Error ? error.message : String(error),
status: (error as any).status,
},
);
// Check if we should retry
if (attempts <= maxRetries) {
// Exponential backoff with jitter
const delay =
retryDelay *
Math.pow(2, attempts - 1) *
(0.5 + Math.random() * 0.5);
logger.debug(`Retrying in ${Math.round(delay)}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
// All attempts failed
return {
success: false,
error: {
code: 'request_failed',
message: lastError?.message || 'Request failed after multiple attempts',
status: (lastError as any)?.status,
},
};
}
}