server.mjs
Rama main · réplica de lectura del repositorio original.
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { Readable } from 'node:stream';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MIME_TYPES = {
'.js': 'application/javascript; charset=utf-8',
'.mjs': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.json': 'application/json; charset=utf-8',
};
const CLIENT_DIR = path.join(__dirname, 'dist', 'client');
const sockets = new Set();
function serveStatic(url, response) {
const pathname = url.pathname;
const filePath = path.join(CLIENT_DIR, pathname);
// Prevent path traversal
if (!filePath.startsWith(CLIENT_DIR)) return false;
let stat;
try {
stat = fs.statSync(filePath);
} catch {
return false;
}
if (!stat.isFile()) return false;
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
// Long-lived cache for hashed assets
const isHashed = /\.[a-zA-Z0-9]{8,}\.\w+$/.test(pathname);
const cacheControl = isHashed
? 'public, max-age=31536000, immutable'
: 'public, max-age=3600';
response.writeHead(200, {
'content-type': contentType,
'cache-control': cacheControl,
'content-length': stat.size,
});
fs.createReadStream(filePath).pipe(response);
return true;
}
function loadEnvFile(filename) {
const filePath = path.join(__dirname, filename);
if (!fs.existsSync(filePath)) return;
const contents = fs.readFileSync(filePath, 'utf8');
for (const rawLine of contents.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const separatorIndex = line.indexOf('=');
if (separatorIndex === -1) continue;
const key = line.slice(0, separatorIndex).trim();
let value = line.slice(separatorIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (!(key in process.env)) {
process.env[key] = value;
}
}
}
loadEnvFile('.env');
loadEnvFile('.env.local');
const { default: serverEntry } = await import('./dist/server/server.js');
const host = process.env.HOST ?? '0.0.0.0';
const port = Number.parseInt(process.env.PORT ?? '3200', 10);
function getOrigin(request) {
const forwardedProto = request.headers['x-forwarded-proto'];
const protocol = Array.isArray(forwardedProto)
? forwardedProto[0]
: (forwardedProto?.split(',')[0] ?? 'http');
const forwardedHost = request.headers['x-forwarded-host'];
const hostHeader = Array.isArray(forwardedHost)
? forwardedHost[0]
: (forwardedHost ?? request.headers.host ?? `${host}:${port}`);
return `${protocol}://${hostHeader}`;
}
const nodeServer = http.createServer(async (request, response) => {
try {
const url = new URL(request.url ?? '/', getOrigin(request));
if (url.pathname === '/healthz') {
response.writeHead(204, {
'cache-control': 'no-store, no-cache, must-revalidate',
});
response.end();
return;
}
if (serveStatic(url, response)) return;
const headers = new Headers();
for (const [key, value] of Object.entries(request.headers)) {
if (value == null) continue;
if (Array.isArray(value)) {
for (const item of value) headers.append(key, item);
} else {
headers.set(key, value);
}
}
const webRequest = new Request(url, {
method: request.method,
headers,
body:
request.method &&
!['GET', 'HEAD'].includes(request.method.toUpperCase())
? Readable.toWeb(request)
: undefined,
duplex: 'half',
});
const webResponse = await serverEntry.fetch(webRequest);
response.statusCode = webResponse.status;
response.statusMessage = webResponse.statusText;
webResponse.headers.forEach((value, key) => {
response.setHeader(key, value);
});
if (!webResponse.body) {
response.end();
return;
}
// Pipe the SSR/web-response stream, handling errors on the source. The
// router's SSR transform errors this stream if it exceeds its lifetime
// (default 120s); without a handler that surfaces as an unhandled 'error'
// event and crashes the whole process. Instead, log it and tear down just
// this response.
const bodyStream = Readable.fromWeb(webResponse.body);
bodyStream.on('error', (streamError) => {
console.error('Aurora response stream error:', streamError);
if (!response.headersSent) response.statusCode = 500;
response.destroy();
});
// If the client disconnects, stop pulling from the source.
response.on('close', () => bodyStream.destroy());
bodyStream.pipe(response);
} catch (error) {
console.error(error);
response.statusCode = 500;
response.setHeader('content-type', 'text/plain; charset=utf-8');
response.end('Aurora failed to handle the request.');
}
});
nodeServer.on('connection', (socket) => {
sockets.add(socket);
socket.on('close', () => {
sockets.delete(socket);
});
});
nodeServer.on('error', (error) => {
console.error(error);
process.exit(1);
});
// Last-resort safety net: a single request's stray async/stream error must not
// take the whole container down. These are per-request faults (e.g. an SSR
// stream aborting), not global state corruption, so log and keep serving.
process.on('uncaughtException', (error) => {
console.error('Uncaught exception (kept server alive):', error);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection (kept server alive):', reason);
});
let shuttingDown = false;
function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
console.log(`\nReceived ${signal}. Shutting down Aurora server...`);
nodeServer.close((error) => {
if (error) {
console.error(error);
process.exit(1);
}
process.exit(0);
});
setTimeout(() => {
for (const socket of sockets) socket.destroy();
}, 5000).unref();
}
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
process.on(signal, () => shutdown(signal));
}
nodeServer.listen(port, host, () => {
console.log(`Aurora listening on http://${host}:${port}`);
});