graceful shutdown and rate limit implementation

This commit is contained in:
2026-07-17 14:23:52 +02:00
parent 06b16b9033
commit 80b73b62e4
4 changed files with 62 additions and 7 deletions
+32 -3
View File
@@ -12,6 +12,7 @@
"axios": "^1.10.0",
"dotenv": "^17.4.2",
"express": "^5.1.0",
"express-rate-limit": "^8.6.0",
"helmet": "^8.1.0",
"pino": "^10.3.1"
},
@@ -1709,9 +1710,9 @@
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2073,6 +2074,25 @@
"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": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz",
@@ -2560,6 +2580,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"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": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+1
View File
@@ -21,6 +21,7 @@
"axios": "^1.10.0",
"dotenv": "^17.4.2",
"express": "^5.1.0",
"express-rate-limit": "^8.6.0",
"helmet": "^8.1.0",
"pino": "^10.3.1"
},
-3
View File
@@ -21,9 +21,6 @@ router.get('/badge', async (req, res) => {
const safeIcon = escapeSvg(icon);
const iconUrl = `https://cdn.simpleicons.org/${safeIcon}?viewbox=auto&size=${size}`;
const iconResponse = await axios.get(iconUrl, { responseType: 'text' });
if (iconResponse.status !== 200) {
return res.status(404).send('Icon not found');
}
const iconSVG = iconResponse.data;
// if there is no label query, use the title from the SVG
+29 -1
View File
@@ -2,6 +2,7 @@ require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const logger = require('./lib/logger');
const api = require('./api');
const version = require('./lib/version').version();
@@ -9,10 +10,20 @@ const version = require('./lib/version').version();
const app = express();
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({
crossOriginResourcePolicy: false,
}));
app.use(limiter);
app.use('/', api.router);
// 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' });
});
app.listen(PORT, () => {
const server = app.listen(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'));