@pubnub/mcp
Version:
PubNub Model Context Protocol MCP Server for Cursor and Claude
656 lines (538 loc) • 19.4 kB
Markdown
# How to Use the XHR Module in PubNub Functions 2.0
The `xhr` (XMLHttpRequest) module in PubNub Functions allows your serverless code to make outbound HTTP/S requests to external APIs, webhooks, databases, or your own backend services. This is essential for integrating with third-party services and building comprehensive real-time applications.
## Requiring the XHR Module
To use the `xhr` module, you first need to require it in your Function:
```javascript
const xhr = require("xhr");
```
## Core Method: `xhr.fetch()`
The primary method provided by the `xhr` module is `fetch()`. It behaves similarly to the standard Fetch API but is optimized for the PubNub Functions environment.
* **Signature:** `xhr.fetch(url, http_options?)`
* `url` (String): The URL of the external resource to request.
* `http_options` (Object, optional): Configuration object for the request.
* **Returns:** A Promise that resolves to a response object from the external server.
### Response Object Properties
The response object contains:
* `status` (Number): HTTP status code (e.g., 200, 404, 500)
* `body` (String): Response body as a string
* `headers` (Object): Response headers as key-value pairs
### HTTP Options Object
The `http_options` parameter can include:
* `method` (String): HTTP method (`"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, etc.) - defaults to `"GET"`
* `headers` (Object): Request headers as key-value pairs
* `body` (String): Request body (for POST/PUT requests)
* `timeout` (Number): Request timeout in milliseconds (if supported)
## Using `async/await`
All `xhr.fetch()` calls return a Promise, so you should always use `async/await` with `try/catch` for error handling.
## Basic Examples
### Example 1: Simple GET Request
```javascript
export default async (request) => {
const xhr = require('xhr');
try {
const apiUrl = 'https://api.quotable.io/random';
console.log(`Fetching data from: ${apiUrl}`);
const serverResponse = await xhr.fetch(apiUrl);
console.log('Status:', serverResponse.status);
if (serverResponse.status === 200) {
const quoteData = JSON.parse(serverResponse.body);
console.log('Quote:', quoteData.content);
console.log('Author:', quoteData.author);
// Add quote to the original message
request.message.dailyQuote = {
text: quoteData.content,
author: quoteData.author,
retrievedAt: new Date().toISOString()
};
} else {
console.error('API request failed with status:', serverResponse.status);
console.error('Response body:', serverResponse.body);
}
return request.ok();
} catch (error) {
console.error('XHR request failed:', error);
return request.abort();
}
};
```
### Example 2: POST Request with JSON Body
```javascript
export default async (request) => {
const xhr = require('xhr');
const vault = require('vault');
try {
// Get webhook URL from vault for security
const webhookUrl = await vault.get("slack_webhook_url");
if (!webhookUrl) {
console.error("Slack webhook URL not configured");
return request.abort();
}
const messageData = {
text: `Alert: ${request.message.alert}`,
channel: '#alerts',
username: 'PubNub Bot',
attachments: [{
color: request.message.severity === 'high' ? 'danger' : 'warning',
fields: [{
title: 'Details',
value: request.message.details,
short: false
}, {
title: 'Timestamp',
value: new Date().toISOString(),
short: true
}]
}]
};
const http_options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'PubNub-Function/2.0'
},
body: JSON.stringify(messageData)
};
const response = await xhr.fetch(webhookUrl, http_options);
if (response.status === 200) {
console.log('Slack notification sent successfully');
} else {
console.error('Slack notification failed:', response.status, response.body);
}
return request.ok();
} catch (error) {
console.error('Slack integration error:', error);
return request.ok(); // Don't block the original message
}
};
```
### Example 3: API Authentication with Headers
```javascript
export default async (request, response) => {
const xhr = require('xhr');
const vault = require('vault');
try {
// Retrieve API key securely from vault
const apiKey = await vault.get("github_api_token");
if (!apiKey) {
return response.send({ error: "API key not configured" }, 500);
}
const username = request.query.username;
if (!username) {
return response.send({ error: "Username required" }, 400);
}
const http_options = {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Accept": "application/vnd.github.v3+json",
"User-Agent": "PubNub-Function"
}
};
const githubUrl = `https://api.github.com/users/${username}`;
const githubResponse = await xhr.fetch(githubUrl, http_options);
if (githubResponse.status === 200) {
const userData = JSON.parse(githubResponse.body);
const userInfo = {
username: userData.login,
name: userData.name,
bio: userData.bio,
publicRepos: userData.public_repos,
followers: userData.followers,
avatarUrl: userData.avatar_url
};
return response.send(userInfo, 200);
} else if (githubResponse.status === 404) {
return response.send({ error: "User not found" }, 404);
} else {
console.error('GitHub API error:', githubResponse.status, githubResponse.body);
return response.send({ error: "Failed to fetch user data" }, 502);
}
} catch (error) {
console.error('Error fetching GitHub user:', error);
return response.send({ error: "Internal server error" }, 500);
}
};
```
## Advanced Integration Examples
### Example 4: Database Integration with Error Handling
```javascript
export default async (request) => {
const xhr = require('xhr');
const vault = require('vault');
const crypto = require('crypto');
try {
// Get database credentials from vault
const [dbUrl, dbApiKey] = await Promise.all([
vault.get("database_api_url"),
vault.get("database_api_key")
]);
if (!dbUrl || !dbApiKey) {
console.error("Database configuration missing");
return request.abort();
}
const userId = request.message.userId;
const userData = request.message.userData;
// Prepare database update
const dbPayload = {
query: "UPDATE users SET last_login = NOW(), profile_data = ? WHERE user_id = ?",
params: [JSON.stringify(userData), userId]
};
const http_options = {
method: 'POST',
headers: {
'Authorization': `Bearer ${dbApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(dbPayload)
};
const dbResponse = await xhr.fetch(dbUrl, http_options);
if (dbResponse.status === 200) {
const result = JSON.parse(dbResponse.body);
if (result.success) {
console.log(`User ${userId} profile updated successfully`);
request.message.dbUpdateSuccess = true;
} else {
console.error('Database update failed:', result.error);
request.message.dbUpdateSuccess = false;
}
} else {
console.error('Database API error:', dbResponse.status, dbResponse.body);
request.message.dbUpdateSuccess = false;
}
return request.ok();
} catch (error) {
console.error('Database integration error:', error);
request.message.dbUpdateSuccess = false;
return request.ok(); // Continue processing even if DB update fails
}
};
```
### Example 5: Multi-Service Integration with Parallel Requests
```javascript
export default async (request) => {
const xhr = require('xhr');
const vault = require('vault');
try {
// Get API keys for multiple services
const [weatherApiKey, newsApiKey, stockApiKey] = await Promise.all([
vault.get("openweather_api_key"),
vault.get("news_api_key"),
vault.get("stock_api_key")
]);
const city = request.message.city || 'New York';
const symbol = request.message.stockSymbol || 'AAPL';
// Prepare multiple API requests
const requests = [];
// Weather API request
if (weatherApiKey) {
requests.push({
name: 'weather',
promise: xhr.fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${weatherApiKey}&units=metric`)
});
}
// News API request
if (newsApiKey) {
requests.push({
name: 'news',
promise: xhr.fetch(`https://newsapi.org/v2/top-headlines?country=us&pageSize=5&apiKey=${newsApiKey}`)
});
}
// Stock API request
if (stockApiKey) {
requests.push({
name: 'stock',
promise: xhr.fetch(`https://api.example.com/stock/${symbol}?apikey=${stockApiKey}`)
});
}
// Execute all requests in parallel
const results = await Promise.allSettled(requests.map(req => req.promise));
// Process results
const enrichmentData = {};
for (let i = 0; i < results.length; i++) {
const result = results[i];
const requestName = requests[i].name;
if (result.status === 'fulfilled' && result.value.status === 200) {
const data = JSON.parse(result.value.body);
switch (requestName) {
case 'weather':
enrichmentData.weather = {
city: data.name,
temperature: data.main.temp,
description: data.weather[0].description,
humidity: data.main.humidity
};
break;
case 'news':
enrichmentData.news = data.articles.slice(0, 3).map(article => ({
title: article.title,
description: article.description,
url: article.url
}));
break;
case 'stock':
enrichmentData.stock = {
symbol: data.symbol,
price: data.price,
change: data.change,
changePercent: data.changePercent
};
break;
}
console.log(`Successfully enriched with ${requestName} data`);
} else {
console.error(`Failed to get ${requestName} data:`, result.reason || result.value?.status);
}
}
// Add enrichment data to the message
request.message.enrichment = {
...enrichmentData,
retrievedAt: new Date().toISOString(),
servicesQueried: requests.length
};
return request.ok();
} catch (error) {
console.error('Multi-service enrichment error:', error);
return request.ok(); // Continue processing even if enrichment fails
}
};
```
### Example 6: Webhook Verification and Response
```javascript
export default async (request, response) => {
const xhr = require('xhr');
const vault = require('vault');
const crypto = require('crypto');
try {
// Verify webhook signature (GitHub example)
const webhookSecret = await vault.get("github_webhook_secret");
if (!webhookSecret) {
return response.send({ error: "Webhook secret not configured" }, 500);
}
const signature = request.headers['x-hub-signature-256'];
const payload = request.body;
if (signature) {
const expectedSignature = await crypto.hmac(webhookSecret, payload, crypto.ALGORITHM.HMAC_SHA256);
const expectedHeader = `sha256=${expectedSignature}`;
if (signature !== expectedHeader) {
console.error("Webhook signature validation failed");
return response.send({ error: "Invalid signature" }, 401);
}
}
const webhookData = JSON.parse(payload);
console.log("Processing webhook:", webhookData.action);
// Process different webhook events
if (webhookData.action === 'opened' && webhookData.pull_request) {
// New pull request opened - notify team
const slackWebhookUrl = await vault.get("slack_webhook_url");
if (slackWebhookUrl) {
const slackMessage = {
text: `🔄 New Pull Request`,
attachments: [{
color: 'good',
title: webhookData.pull_request.title,
title_link: webhookData.pull_request.html_url,
fields: [{
title: 'Author',
value: webhookData.pull_request.user.login,
short: true
}, {
title: 'Repository',
value: webhookData.repository.name,
short: true
}]
}]
};
const notificationResponse = await xhr.fetch(slackWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(slackMessage)
});
if (notificationResponse.status === 200) {
console.log("Team notified via Slack");
}
}
}
return response.send({ message: "Webhook processed successfully" }, 200);
} catch (error) {
console.error('Webhook processing error:', error);
return response.send({ error: "Internal server error" }, 500);
}
};
```
### Example 7: Rate-Limited API Integration
```javascript
export default async (request) => {
const xhr = require('xhr');
const vault = require('vault');
const db = require('kvstore');
try {
const apiKey = await vault.get("rate_limited_api_key");
if (!apiKey) {
console.error("API key not configured");
return request.abort();
}
// Check rate limit (example: 100 requests per hour)
const rateLimitKey = "api_rate_limit_counter";
const currentHour = Math.floor(Date.now() / (1000 * 60 * 60));
const hourlyKey = `${rateLimitKey}:${currentHour}`;
const currentCount = await db.getCounter(hourlyKey);
if (currentCount >= 100) {
console.log("API rate limit exceeded, skipping request");
request.message.apiSkipped = true;
request.message.rateLimitExceeded = true;
return request.ok();
}
// Make API request
const apiUrl = `https://api.example.com/data/${request.message.dataId}`;
const http_options = {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/json'
}
};
const apiResponse = await xhr.fetch(apiUrl, http_options);
if (apiResponse.status === 200) {
// Increment rate limit counter with 2-hour TTL
await db.incrCounter(hourlyKey);
await db.set(`${hourlyKey}_ttl`, Date.now(), 120); // 2 hour TTL for cleanup
const apiData = JSON.parse(apiResponse.body);
request.message.apiData = apiData;
request.message.apiSkipped = false;
console.log("API data retrieved successfully");
} else if (apiResponse.status === 429) {
// API returned rate limit error
console.log("API rate limit hit, will retry later");
request.message.apiSkipped = true;
request.message.rateLimitHit = true;
} else {
console.error("API request failed:", apiResponse.status, apiResponse.body);
request.message.apiSkipped = true;
request.message.apiError = true;
}
return request.ok();
} catch (error) {
console.error('Rate-limited API integration error:', error);
request.message.apiSkipped = true;
request.message.apiError = true;
return request.ok();
}
};
```
## Error Handling and Best Practices
### Common HTTP Status Codes and Handling
```javascript
export default async (request) => {
const xhr = require('xhr');
try {
const response = await xhr.fetch('https://api.example.com/data');
switch (response.status) {
case 200:
// Success
const data = JSON.parse(response.body);
console.log('Request successful');
break;
case 400:
// Bad Request
console.error('Bad request - check your parameters');
break;
case 401:
// Unauthorized
console.error('API authentication failed');
break;
case 403:
// Forbidden
console.error('API access forbidden');
break;
case 404:
// Not Found
console.error('API endpoint or resource not found');
break;
case 429:
// Too Many Requests
console.error('API rate limit exceeded');
break;
case 500:
// Internal Server Error
console.error('API server error');
break;
default:
console.error('Unexpected status code:', response.status);
}
return request.ok();
} catch (error) {
console.error('Network or parsing error:', error);
return request.abort();
}
};
```
### Best Practices
#### 1. **Always Use HTTPS**
```javascript
// Good
const apiUrl = 'https://api.example.com/data';
// Avoid HTTP for sensitive data
const apiUrl = 'http://api.example.com/data'; // ❌
```
#### 2. **Handle Timeouts and Network Errors**
```javascript
try {
const response = await xhr.fetch(apiUrl, {
timeout: 10000 // 10 seconds
});
} catch (error) {
if (error.code === 'TIMEOUT') {
console.error('Request timed out');
} else {
console.error('Network error:', error);
}
}
```
#### 3. **Set Appropriate Headers**
```javascript
const http_options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'PubNub-Function/2.0',
'Accept': 'application/json'
},
body: JSON.stringify(data)
};
```
#### 4. **Validate Response Data**
```javascript
if (response.status === 200) {
try {
const data = JSON.parse(response.body);
if (data && data.results) {
// Process valid data
} else {
console.error('Invalid response format');
}
} catch (parseError) {
console.error('JSON parsing failed:', parseError);
}
}
```
#### 5. **Use Parallel Requests Efficiently**
```javascript
// Efficient - run in parallel
const [response1, response2] = await Promise.all([
xhr.fetch(url1),
xhr.fetch(url2)
]);
// Less efficient - sequential
const response1 = await xhr.fetch(url1);
const response2 = await xhr.fetch(url2);
```
## Important Considerations
* **Operation Limits:** XHR operations count toward the 3-operation limit per function execution
* **Timeout:** Requests have built-in timeouts (typically 5-10 seconds)
* **No Redirects:** The XHR module does not automatically follow HTTP redirects
* **Size Limits:** Response bodies may have size limitations
* **CORS:** CORS restrictions don't apply to server-side requests from Functions
* **Security:** Always use HTTPS for sensitive data and validate responses
* **Rate Limiting:** Be mindful of external API rate limits and implement appropriate handling
* **Error Handling:** Always wrap XHR calls in try/catch blocks and handle various HTTP status codes appropriately
The XHR module is a powerful tool for integrating PubNub Functions with external services, enabling you to build comprehensive real-time applications that can interact with any HTTP-based API or service.