速率限制
為確保服務穩定,API 有請求頻率限制。
每個 API 回應都包含速率限制資訊:
X-RateLimit-Limit: 60X-RateLimit-Remaining: 45X-RateLimit-Reset: 1705312860| 標頭 | 說明 |
|---|---|
| X-RateLimit-Limit | 每分鐘請求上限 |
| X-RateLimit-Remaining | 剩餘可用次數 |
| X-RateLimit-Reset | 重置時間(Unix timestamp) |
當超過速率限制時,API 回傳:
HTTP/1.1 429 Too Many Requests{ "error": "Rate limit exceeded", "retryAfter": 45}retryAfter 表示需要等待的秒數。
監控剩餘次數
Section titled “監控剩餘次數”async function apiRequest(url) { const response = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
const remaining = response.headers.get('X-RateLimit-Remaining'); if (remaining && parseInt(remaining) < 10) { console.warn(`Rate limit warning: ${remaining} requests remaining`); }
return response;}指數退避重試
Section titled “指數退避重試”async function fetchWithRetry(url, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
if (response.status === 429) { const retryAfter = response.headers.get('Retry-After') || 60; const delay = Math.min( parseInt(retryAfter) * 1000, Math.pow(2, attempt) * 1000 );
console.log(`Rate limited. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); continue; }
return response; }
throw new Error('Max retries exceeded');}class RateLimitedQueue { constructor(maxPerMinute = 60) { this.queue = []; this.processing = false; this.interval = 60000 / maxPerMinute; }
async add(fn) { return new Promise((resolve, reject) => { this.queue.push({ fn, resolve, reject }); this.process(); }); }
async process() { if (this.processing || this.queue.length === 0) return;
this.processing = true; const { fn, resolve, reject } = this.queue.shift();
try { const result = await fn(); resolve(result); } catch (error) { reject(error); }
setTimeout(() => { this.processing = false; this.process(); }, this.interval); }}
// 使用const queue = new RateLimitedQueue(60);
async function fetchChannel(id) { return queue.add(() => fetch(`/api/channels/${id}`, { headers: { Authorization: `Bearer ${API_KEY}` } }) );}// 不好:每筆資料一個請求for (const id of channelIds) { await fetch(`/api/channels/${id}`);}
// 好:使用列表 APIconst response = await fetch('/api/channels');const { channels } = await response.json();const cache = new Map();
async function getChannel(id) { if (cache.has(id)) { const { data, timestamp } = cache.get(id); if (Date.now() - timestamp < 60000) { return data; } }
const response = await fetch(`/api/channels/${id}`); const data = await response.json();
cache.set(id, { data, timestamp: Date.now() }); return data;}使用 Webhook
Section titled “使用 Webhook”對於即時資料,使用 Webhook 而非輪詢:
// 不好:每秒輪詢setInterval(async () => { await fetch('/api/channels/C123/backup/messages');}, 1000);
// 好:使用 Webhook 接收事件app.post('/webhook', (req, res) => { handleNewMessage(req.body); res.status(200).send('OK');});