Ctrl K
ComunidadGuías…

server.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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}`);
});