Skip to main content
Glama
reddit-api.ts13.9 kB
/** * Reddit API client with built-in error handling and retries */ import { RedditPost, RedditComment, RedditUser, RedditSubreddit, RedditListing } from '../types/reddit.types.js'; import { AuthManager } from '../core/auth.js'; import { RateLimiter } from '../core/rate-limiter.js'; import { CacheManager } from '../core/cache.js'; export interface RedditAPIOptions { authManager: AuthManager; rateLimiter: RateLimiter; cacheManager: CacheManager; timeout?: number; } export class RedditAPI { private auth: AuthManager; private rateLimiter: RateLimiter; private cache: CacheManager; private timeout: number; private baseUrl = 'https://www.reddit.com'; private oauthUrl = 'https://oauth.reddit.com'; constructor(options: RedditAPIOptions) { this.auth = options.authManager; this.rateLimiter = options.rateLimiter; this.cache = options.cacheManager; this.timeout = options.timeout ?? 10000; // Increased timeout to 10 seconds } /** * Browse a subreddit */ async browseSubreddit( subreddit: string, sort: 'hot' | 'new' | 'top' | 'rising' | 'controversial' = 'hot', options: { limit?: number; time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'; after?: string; } = {} ): Promise<RedditListing<RedditPost>> { // Validate and clean subreddit name if (!subreddit || subreddit.trim() === '') { throw new Error('Subreddit name is required. Try "programming", "technology", "news", or "AskReddit"'); } // Clean up subreddit name (remove r/ prefix if present, trim whitespace) subreddit = subreddit.replace(/^r\//, '').trim(); const { limit = 10, time, after } = options; // Build cache key const cacheKey = CacheManager.createKey('subreddit', subreddit, sort, limit, time, after); // Check cache const cached = this.cache.get<RedditListing<RedditPost>>(cacheKey); if (cached) { return cached; } // Build URL const params = new URLSearchParams({ limit: String(limit), raw_json: '1', // Avoid HTML entities }); if (time && (sort === 'top' || sort === 'controversial')) { params.append('t', time); } if (after) { params.append('after', after); } // All subreddits use /r/ prefix const endpoint = `/r/${subreddit}/${sort}.json`; // Make request const data = await this.get<RedditListing<RedditPost>>( `${endpoint}?${params.toString()}` ); // Cache result this.cache.set(cacheKey, data); return data; } /** * Get post details with comments */ async getPost( postId: string, options: { limit?: number; sort?: 'best' | 'top' | 'new' | 'controversial' | 'qa'; depth?: number; } = {} ): Promise<[RedditListing<RedditPost>, RedditListing<RedditComment>]> { const { limit = 50, sort = 'best', depth = 3 } = options; // Extract subreddit and post ID from various formats let subreddit: string; let id: string; if (postId.includes('/comments/')) { // Full URL format const match = postId.match(/\/r\/(\w+)\/comments\/(\w+)/); if (match) { subreddit = match[1]; id = match[2]; } else { throw new Error('Invalid post URL format'); } } else if (postId.includes('_')) { // Format: subreddit_postid [subreddit, id] = postId.split('_'); } else { // Just the ID, need to fetch subreddit via /api/info id = postId; // Fetch post info to get subreddit const infoData = await this.get<RedditListing<RedditPost>>( `/api/info.json?id=t3_${id}` ); if (!infoData.data.children.length) { throw new Error(`Post with ID ${id} not found`); } subreddit = infoData.data.children[0].data.subreddit; } const cacheKey = CacheManager.createKey('post', subreddit, id, sort, limit, depth); const cached = this.cache.get<[RedditListing<RedditPost>, RedditListing<RedditComment>]>(cacheKey); if (cached) { return cached; } const params = new URLSearchParams({ limit: String(limit), sort, depth: String(depth), raw_json: '1', }); const data = await this.get<[RedditListing<RedditPost>, RedditListing<RedditComment>]>( `/r/${subreddit}/comments/${id}.json?${params.toString()}` ); this.cache.set(cacheKey, data); return data; } /** * Search Reddit */ async search( query: string, options: { subreddit?: string; sort?: 'relevance' | 'hot' | 'top' | 'new' | 'comments'; time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'; limit?: number; after?: string; restrictSr?: boolean; } = {} ): Promise<RedditListing<RedditPost>> { const { subreddit, sort = 'relevance', time = 'all', limit = 10, after, restrictSr = !!subreddit } = options; const cacheKey = CacheManager.createKey('search', query, subreddit, sort, time, limit, after); const cached = this.cache.get<RedditListing<RedditPost>>(cacheKey); if (cached) { return cached; } const params = new URLSearchParams({ q: query, sort, t: time, limit: String(limit), restrict_sr: String(restrictSr), raw_json: '1', }); if (after) { params.append('after', after); } const endpoint = subreddit ? `/r/${subreddit}/search.json` : '/search.json'; const data = await this.get<RedditListing<RedditPost>>( `${endpoint}?${params.toString()}` ); this.cache.set(cacheKey, data); return data; } /** * Get user information */ async getUser(username: string): Promise<RedditUser> { const cacheKey = CacheManager.createKey('user', username); const cached = this.cache.get<RedditUser>(cacheKey); if (cached) { return cached; } const data = await this.get<{ data: RedditUser }>( `/user/${username}/about.json` ); const user = data.data; this.cache.set(cacheKey, user); return user; } /** * Get user's recent posts */ async getUserPosts( username: string, type: 'submitted' | 'comments' = 'submitted', options: { sort?: 'new' | 'top' | 'hot'; time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'; limit?: number; } = {} ): Promise<RedditListing<RedditPost | RedditComment>> { const { sort = 'new', time = 'all', limit = 10 } = options; const cacheKey = CacheManager.createKey('user-posts', username, type, sort, time, limit); const cached = this.cache.get<RedditListing<RedditPost | RedditComment>>(cacheKey); if (cached) { return cached; } const params = new URLSearchParams({ sort, t: time, limit: String(limit), raw_json: '1', }); const data = await this.get<RedditListing<RedditPost | RedditComment>>( `/user/${username}/${type}.json?${params.toString()}` ); this.cache.set(cacheKey, data); return data; } /** * Get subreddit information */ async getSubreddit(name: string): Promise<RedditSubreddit> { const cacheKey = CacheManager.createKey('subreddit-info', name); const cached = this.cache.get<RedditSubreddit>(cacheKey); if (cached) { return cached; } const data = await this.get<{ data: RedditSubreddit }>( `/r/${name}/about.json` ); const subreddit = data.data; this.cache.set(cacheKey, subreddit); return subreddit; } /** * Get trending subreddits */ async getTrending(): Promise<string[]> { const cacheKey = CacheManager.createKey('trending'); const cached = this.cache.get<string[]>(cacheKey); if (cached) { return cached; } // Reddit's trending API has been deprecated, so we'll use popular subreddits const popular = await this.browseSubreddit('popular', 'hot', { limit: 25 }); // Extract unique subreddits from popular posts const subreddits = new Set<string>(); popular.data.children.forEach(child => { subreddits.add(child.data.subreddit); }); const trending = Array.from(subreddits).slice(0, 10); this.cache.set(cacheKey, trending); return trending; } /** * Private: Make GET request to Reddit API with retry logic */ private async get<T>(endpoint: string, retries: number = 2): Promise<T> { // Check rate limit if (!this.rateLimiter.canMakeRequest()) { const isAuth = this.auth.isAuthenticated(); throw new Error(this.rateLimiter.getErrorMessage(isAuth)); } // Get headers (includes auth if available) let headers = await this.auth.getHeaders(); // Determine base URL const isAuthenticated = this.auth.isAuthenticated(); const baseUrl = isAuthenticated ? this.oauthUrl : this.baseUrl; // Create abort controller for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(`${baseUrl}${endpoint}`, { headers, signal: controller.signal, }); clearTimeout(timeoutId); // Record successful request this.rateLimiter.recordRequest(); // Handle auth token expiry if (response.status === 401 && isAuthenticated && retries > 0) { // Token might be expired, try refreshing try { await this.auth.refreshAccessToken(); headers = await this.auth.getHeaders(); // Retry with new token return this.get<T>(endpoint, retries - 1); } catch (refreshError) { throw new Error('Authentication failed. Please run: reddit-mcp-buddy --auth'); } } // Handle errors if (!response.ok) { if (response.status === 404) { // Extract subreddit name from URL if possible const subredditMatch = endpoint.match(/\/r\/([^\/]+)/); const subredditName = subredditMatch ? subredditMatch[1] : 'resource'; throw new Error(`Not found - r/${subredditName} does not exist or is inaccessible`); } if (response.status === 403) { // For 403, try to determine if it's a non-existent subreddit or private const subredditMatch = endpoint.match(/\/r\/([^\/]+)/); const subredditName = subredditMatch ? subredditMatch[1] : null; if (subredditName) { // Common issue: Reddit returns 403 for both non-existent and private subreddits throw new Error(`Cannot access r/${subredditName} - it may be private, quarantined, or doesn't exist. Try a public subreddit like 'programming', 'technology', or 'news'`); } throw new Error('Access forbidden - the requested content may be private or restricted'); } if (response.status === 429) { throw new Error('Rate limited by Reddit - please wait before trying again'); } if (response.status === 503) { throw new Error('Reddit is temporarily unavailable - please try again later'); } let errorText = ''; try { errorText = await response.text(); } catch { errorText = 'Unable to read error response'; } throw new Error(`Reddit API error (${response.status}): ${errorText}`); } // Try to parse JSON const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { throw new Error('Reddit returned HTML instead of JSON - the subreddit may be inaccessible or there may be a Reddit issue'); } const data = await response.json(); return data as T; } catch (error: any) { clearTimeout(timeoutId); // Log the actual error for debugging console.error('Reddit API Error:', error.message || error); if (error.name === 'AbortError') { throw new Error('Request timeout (10s exceeded) - Reddit may be slow or unreachable. Try again or check if Reddit is blocked on your network.'); } // Common network errors if (error.code === 'ENOTFOUND') { throw new Error('Cannot resolve Reddit domain - check DNS settings or if Reddit is blocked by your ISP/firewall'); } if (error.code === 'ECONNREFUSED') { throw new Error('Connection refused - Reddit may be blocked by firewall or network policy'); } if (error.code === 'ETIMEDOUT' || error.code === 'ECONNRESET') { throw new Error('Connection timeout - Reddit may be blocked or network is unstable'); } if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') { throw new Error('SSL certificate error - may be caused by proxy or firewall'); } // Fetch-specific errors if (error.cause?.code === 'ENOTFOUND') { throw new Error(`Cannot reach Reddit servers - ${error.cause.hostname || 'reddit.com'} is not accessible. Check if Reddit is blocked in your region/network.`); } if (error.message?.includes('fetch failed')) { // Extract more details from the fetch error const details = error.cause ? ` (${error.cause.code || error.cause.message})` : ''; throw new Error(`Failed to connect to Reddit${details}. Common causes: firewall blocking, geo-restriction, or ISP blocking Reddit.`); } if (error.message?.includes('fetch')) { throw new Error(`Network error accessing Reddit: ${error.message}. If Reddit works in your browser, try using a VPN.`); } // Pass through the original error if we don't have a specific handler throw error; } } }

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/karanb192/reddit-buddy-mcp'

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