dns retry
This commit is contained in:
104
server.js
104
server.js
@@ -3,6 +3,7 @@ const webpush = require('web-push');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const dns = require('dns'); // Add DNS module
|
||||
|
||||
// Load environment variables from .env file
|
||||
require('dotenv').config();
|
||||
@@ -19,7 +20,26 @@ const flicSecret = process.env.FLIC_SECRET; // Optional Bearer token secret for
|
||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(',').map(origin => origin.trim()).filter(origin => origin);
|
||||
const allowedMethods = (process.env.ALLOWED_METHODS || "POST,OPTIONS,GET").split(',').map(method => method.trim()).filter(method => method);
|
||||
const allowedHeaders = (process.env.ALLOWED_HEADERS || "Content-Type,Authorization").split(',').map(header => header.trim()).filter(header => header);
|
||||
// Retry configuration for DNS resolution issues
|
||||
const maxRetries = parseInt(process.env.MAX_NOTIFICATION_RETRIES || 3, 10);
|
||||
const initialRetryDelay = parseInt(process.env.INITIAL_RETRY_DELAY_MS || 1000, 10); // 1 second
|
||||
// HTTP request timeout configuration
|
||||
const httpTimeout = parseInt(process.env.HTTP_TIMEOUT_MS || 10000, 10); // 10 seconds
|
||||
|
||||
// Configure global HTTP agent with timeouts to prevent hanging requests
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// Custom HTTPS agent with timeout
|
||||
const httpsAgent = new https.Agent({
|
||||
keepAlive: true,
|
||||
timeout: httpTimeout,
|
||||
maxSockets: 50, // Limit concurrent connections
|
||||
});
|
||||
|
||||
// Apply the agent to the webpush module if possible
|
||||
// Note: The web-push library might use its own agent, but this is a precaution
|
||||
https.globalAgent = httpsAgent;
|
||||
|
||||
// --- Validation ---
|
||||
if (!vapidPublicKey || !vapidPrivateKey || !vapidSubject) {
|
||||
@@ -34,6 +54,43 @@ webpush.setVapidDetails(
|
||||
vapidPrivateKey
|
||||
);
|
||||
|
||||
// Configure DNS settings for more reliable resolution in containerized environments
|
||||
// These settings can help with temporary DNS resolution failures
|
||||
dns.setDefaultResultOrder('ipv4first'); // Prefer IPv4 to avoid some IPv6 issues in containers
|
||||
const dnsTimeout = parseInt(process.env.DNS_TIMEOUT_MS || 5000, 10);
|
||||
dns.setServers(dns.getServers()); // Reset DNS servers (can help in some Docker environments)
|
||||
|
||||
// You can optionally configure a specific DNS server if needed:
|
||||
// Example: dns.setServers(['8.8.8.8', '1.1.1.1']);
|
||||
|
||||
// --- Utility function for retrying web push notifications with exponential backoff ---
|
||||
async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay = initialRetryDelay) {
|
||||
try {
|
||||
return await webpush.sendNotification(subscription, payload);
|
||||
} catch (error) {
|
||||
// Check if the error is a DNS resolution error that might be temporary
|
||||
const isDnsError = error.code === 'EAI_AGAIN' ||
|
||||
error.code === 'ENOTFOUND' ||
|
||||
error.code === 'ETIMEDOUT';
|
||||
|
||||
if (isDnsError && retryCount < maxRetries) {
|
||||
console.log(`DNS resolution failed (${error.code}). Retrying notification in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`);
|
||||
|
||||
// Wait for the delay
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// Exponential backoff with jitter
|
||||
const nextDelay = delay * (1.5 + Math.random() * 0.5);
|
||||
|
||||
// Retry recursively with increased count and delay
|
||||
return sendWebPushWithRetry(subscription, payload, retryCount + 1, nextDelay);
|
||||
}
|
||||
|
||||
// If we've exhausted retries or it's not a DNS error, rethrow
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Subscription Loading and Management ---
|
||||
let subscriptions = {}; // In-memory cache of subscriptions
|
||||
|
||||
@@ -159,51 +216,54 @@ app.post('/subscribe', async (req, res) => {
|
||||
// --- Flic Webhook Endpoint ---
|
||||
// Apply Flic-specific authentication ONLY to this route
|
||||
app.post('/flic-webhook', authenticateFlicRequest, async (req, res) => {
|
||||
// Assuming Flic sends 'button_id' which is the serial number
|
||||
const { button_id, click_type, timestamp } = req.body;
|
||||
// Get buttonName from Header 'Button-Name' and timestamp from Header 'Timestamp'
|
||||
const buttonName = req.headers['button-name'];
|
||||
const timestamp = req.headers['timestamp'];
|
||||
// Still get click_type from the request body
|
||||
const { click_type } = req.body;
|
||||
|
||||
console.log(`Received webhook: Button=${button_id}, Type=${click_type}, Timestamp=${timestamp || 'N/A'}`);
|
||||
console.log(`Received webhook: Button=${buttonName}, Type=${click_type}, Timestamp=${timestamp || 'N/A'}`);
|
||||
|
||||
// Basic validation
|
||||
if (!button_id || !click_type) {
|
||||
console.warn(`Webhook Error: Missing button_id or click_type`);
|
||||
return res.status(400).json({ message: 'Bad Request: Missing button_id or click_type' });
|
||||
if (!buttonName || !click_type) {
|
||||
console.warn(`Webhook Error: Missing Button-Name header or click_type in body`);
|
||||
return res.status(400).json({ message: 'Bad Request: Missing Button-Name header or click_type in request body' });
|
||||
}
|
||||
|
||||
const normalizedButtonId = button_id.toLowerCase(); // Use lowercase for lookup consistency
|
||||
const normalizedButtonName = buttonName.toLowerCase(); // Use lowercase for lookup consistency
|
||||
|
||||
// Find the subscription associated with this button ID
|
||||
const subscription = subscriptions[normalizedButtonId];
|
||||
// Find the subscription associated with this normalized button name
|
||||
const subscription = subscriptions[normalizedButtonName];
|
||||
|
||||
if (!subscription) {
|
||||
console.warn(`Webhook: No subscription found for button ID: ${normalizedButtonId} (original: ${button_id})`);
|
||||
return res.status(404).json({ message: `Not Found: No subscription configured for button ${normalizedButtonId}` });
|
||||
console.warn(`Webhook: No subscription found for button ID: ${normalizedButtonName} (original: ${buttonName})`);
|
||||
return res.status(404).json({ message: `Not Found: No subscription configured for button ${normalizedButtonName}` });
|
||||
}
|
||||
|
||||
// --- Send Web Push Notification ---
|
||||
const payload = JSON.stringify({
|
||||
title: 'Flic Button Action',
|
||||
body: `Button ${click_type}`, // Simplified body
|
||||
body: `Button ${normalizedButtonName}: ${click_type}`, // Simplified body
|
||||
data: {
|
||||
action: click_type,
|
||||
button: normalizedButtonId, // Send normalized ID
|
||||
button: normalizedButtonName, // Send normalized button name
|
||||
timestamp: timestamp || new Date().toISOString()
|
||||
}
|
||||
// icon: '/path/to/icon.png'
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(`Sending push notification for ${normalizedButtonId} to endpoint: ${subscription.endpoint.substring(0, 40)}...`);
|
||||
await webpush.sendNotification(subscription, payload);
|
||||
console.log(`Push notification sent successfully for button ${normalizedButtonId}.`);
|
||||
console.log(`Sending push notification for ${normalizedButtonName} to endpoint: ${subscription.endpoint.substring(0, 40)}...`);
|
||||
await sendWebPushWithRetry(subscription, payload);
|
||||
console.log(`Push notification sent successfully for button ${normalizedButtonName}.`);
|
||||
res.status(200).json({ message: 'Push notification sent successfully' });
|
||||
} catch (error) {
|
||||
console.error(`Error sending push notification for button ${normalizedButtonId}:`, error.body || error.message || error);
|
||||
console.error(`Error sending push notification for button ${normalizedButtonName}:`, error.body || error.message || error);
|
||||
|
||||
if (error.statusCode === 404 || error.statusCode === 410) {
|
||||
console.warn(`Subscription for button ${normalizedButtonId} is invalid or expired (404/410). Removing it.`);
|
||||
console.warn(`Subscription for button ${normalizedButtonName} is invalid or expired (404/410). Removing it.`);
|
||||
// Optionally remove the stale subscription
|
||||
delete subscriptions[normalizedButtonId];
|
||||
delete subscriptions[normalizedButtonName];
|
||||
saveSubscriptions(); // Attempt to save the updated list
|
||||
res.status(410).json({ message: 'Subscription Gone' });
|
||||
} else {
|
||||
@@ -223,7 +283,6 @@ app.get('/health', (req, res) => {
|
||||
|
||||
// --- Start Server ---
|
||||
// Use http.createServer to allow graceful shutdown
|
||||
const http = require('http');
|
||||
const server = http.createServer(app);
|
||||
|
||||
server.listen(port, () => {
|
||||
@@ -234,6 +293,9 @@ server.listen(port, () => {
|
||||
console.log(`Flic Webhook Auth: ${flicSecret ? 'Enabled (Bearer Token)' : 'Disabled'}`);
|
||||
console.log(`Subscription Endpoint Auth: Disabled`);
|
||||
console.log(`Subscriptions File: ${subscriptionsFilePath}`);
|
||||
console.log(`Push Notification Retry Config: ${maxRetries} retries, ${initialRetryDelay}ms initial delay`);
|
||||
console.log(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);
|
||||
console.log(`HTTP Timeout: ${httpTimeout}ms`);
|
||||
});
|
||||
|
||||
// --- Graceful Shutdown ---
|
||||
@@ -249,7 +311,7 @@ const closeGracefully = (signal) => {
|
||||
setTimeout(() => {
|
||||
console.error('Could not close connections in time, forcefully shutting down');
|
||||
process.exit(1);
|
||||
}, 10000); // 10 seconds timeout
|
||||
}, 10000); // 10 seconds timeout - This is a literal so no need to parse
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => closeGracefully('SIGTERM'));
|
||||
|
||||
Reference in New Issue
Block a user