graceful shutdown and rate limit implementation
This commit is contained in:
Generated
+32
-3
@@ -12,6 +12,7 @@
|
|||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.6.0",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"pino": "^10.3.1"
|
"pino": "^10.3.1"
|
||||||
},
|
},
|
||||||
@@ -1709,9 +1710,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -2073,6 +2074,25 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"ip-address": "^10.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-copy": {
|
"node_modules/fast-copy": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz",
|
||||||
@@ -2560,6 +2580,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.6.0",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"pino": "^10.3.1"
|
"pino": "^10.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ router.get('/badge', async (req, res) => {
|
|||||||
const safeIcon = escapeSvg(icon);
|
const safeIcon = escapeSvg(icon);
|
||||||
const iconUrl = `https://cdn.simpleicons.org/${safeIcon}?viewbox=auto&size=${size}`;
|
const iconUrl = `https://cdn.simpleicons.org/${safeIcon}?viewbox=auto&size=${size}`;
|
||||||
const iconResponse = await axios.get(iconUrl, { responseType: 'text' });
|
const iconResponse = await axios.get(iconUrl, { responseType: 'text' });
|
||||||
if (iconResponse.status !== 200) {
|
|
||||||
return res.status(404).send('Icon not found');
|
|
||||||
}
|
|
||||||
const iconSVG = iconResponse.data;
|
const iconSVG = iconResponse.data;
|
||||||
|
|
||||||
// if there is no label query, use the title from the SVG
|
// if there is no label query, use the title from the SVG
|
||||||
|
|||||||
+29
-1
@@ -2,6 +2,7 @@ require('dotenv').config();
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
const logger = require('./lib/logger');
|
const logger = require('./lib/logger');
|
||||||
const api = require('./api');
|
const api = require('./api');
|
||||||
const version = require('./lib/version').version();
|
const version = require('./lib/version').version();
|
||||||
@@ -9,10 +10,20 @@ const version = require('./lib/version').version();
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Rate limiting – max 100 requests per minute per IP
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000,
|
||||||
|
max: 100,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { error: 'Too many requests, please try again later.' },
|
||||||
|
});
|
||||||
|
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
crossOriginResourcePolicy: false,
|
crossOriginResourcePolicy: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
app.use(limiter);
|
||||||
app.use('/', api.router);
|
app.use('/', api.router);
|
||||||
|
|
||||||
// Central error handler – catches all unhandled errors
|
// Central error handler – catches all unhandled errors
|
||||||
@@ -22,6 +33,23 @@ app.use((err, req, res, next) => {
|
|||||||
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
|
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
const server = app.listen(PORT, () => {
|
||||||
logger.info(`BadgeDex version ${version} listening on http://localhost:${PORT}/`);
|
logger.info(`BadgeDex version ${version} listening on http://localhost:${PORT}/`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown – close server cleanly on SIGTERM / SIGINT
|
||||||
|
const shutdown = (signal) => {
|
||||||
|
logger.info(`${signal} received, shutting down gracefully`);
|
||||||
|
server.close(() => {
|
||||||
|
logger.info('HTTP server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
// Force exit after 5 seconds if connections don't close
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.warn('Forced shutdown after timeout');
|
||||||
|
process.exit(1);
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
|||||||
Reference in New Issue
Block a user