graceful shutdown and rate limit implementation
This commit is contained in:
Generated
+32
-3
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user