TeqFW: servidor web

Fecha de publicación: 2021-12-15

El servidor web TeqFW puede ejecutarse en HTTP/1, HTTP/2 o HTTPS sobre HTTP/2. HTTP/1 es práctico en desarrollo local, HTTP/2 puede estar detrás de otro servidor como nginx, y HTTPS-over-HTTP/2 puede actuar como endpoint público. El servidor se diseña como un pipeline de solicitudes componible, aportado por plugins Teq.

Fundamentos HTTP

HTTP es solicitud-respuesta: línea inicial, cabeceras y opcionalmente cuerpo; la respuesta envía cabeceras antes del cuerpo. El cuerpo puede transmitirse con el tiempo, como en Server-Sent Events. Node.js expone servidores http y http2 mediante eventos como request, connect, error y close.

server.on('request', (req, res) => {
  // Read req and write res.
});

Solicitud y respuesta son streams. Las cabeceras deben decidirse antes de escribir cualquier cuerpo.

Procesamiento de solicitudes

El listener acepta HEAD, GET y POST; otros métodos reciben 405. Para cuerpos POST text/plain y application/json, lee y adjunta texto o JSON parseado. Otros tipos quedan como streams para handlers especializados.

Los plugins aportan handlers. Uno puede reconocer solicitud, añadir cabeceras, adjuntar cuerpo a res.teqBody o ruta de archivo a res.teqFile. El handler final envía primero cabeceras y después transmite archivo o termina con cuerpo. Si nadie reclama, responde 404.

Dispatcher y handlers

TeqFw_Web_Back_Server_Dispatcher descubre e inicializa handlers antes de registrar su listener:

await dispatcher.createHandlers(); server.on('request', dispatcher.getListener());

Un handler implementa inicialización, comprobación de propiedad y procesador:

class RequestHandler {
  async init() {}
  requestIsMine({ method, address, headers } = {}) {}
  getProcessor() { return async (req, res) => {}; }
}

Los descriptores ordenan handlers mediante before/after y reservan espacios de dirección:

{
  "@teqfw/web": {
    "handlers": {
      "Vendor_Back_Handler_Upload": {
        "after": ["TeqFw_Web_Back_Handler_WAPI"],
        "before": ["TeqFw_Web_Back_Handler_Static"],
        "space": ["upload"]
      }
    }
  }
}

Los procesadores se ejecutan en orden y pueden enriquecer req o res para los siguientes. Un handler que responde por sí mismo debe dejarlo claro; los posteriores comprueban res.headersSent.

Respuesta final

El handler final envía cabeceras acumuladas y escoge archivo o cuerpo. Transmite archivos existentes y devuelve 404 si no hay ninguno ni cuerpo. La separación permite que los anteriores se centren en routing y negocio, manteniendo coherente la respuesta HTTP.

Roles integrados

Reservan espacios como sse, api, upload, src y web. El modelo de dirección analiza URL y permite que cada handler reclame solo su área.

Recursos estáticos

Hay dos grupos principales:

Los metadatos de autoload asignan namespace a fuentes. Una URL como /src/@vendor/package/Path/To/Module.mjs puede mapear a archivo del paquete correspondiente. Dependencias no Teq se pueden mapear explícitamente, por ejemplo para exponer archivos Vue seleccionados. Solo se deben exponer raíces estáticas previstas; nunca convierta una ruta amplia del sistema de archivos en espacio URL público.

Web API, SSE y uploads

WAPI posee /api/, acepta GET y POST y devuelve JSON. Los plugins registran módulos de API en el descriptor bajo rutas con namespace. SSE y uploads requieren decisiones adicionales: autenticación, autorización, propiedad de canal compartido, límites, validación de contenido y política de almacenamiento. Los handlers no usados se pueden excluir en configuración.

Resumen

TeqFW ofrece HTTP/1, HTTP/2 y HTTPS mediante pipeline de handlers conectables para estáticos, API JSON, SSE y uploads. Los plugins añaden capacidades sin modificar dispatcher, mientras propiedad de espacios y handler final hacen predecible el procesamiento. En despliegue expuesto a Internet, use TLS, autenticación, límites de tamaño, validación, logs y proxy inverso o controles operativos equivalentes cuando corresponda.

Fragmentos adicionales de código fuente

function process(req, res) {} Подключение обработчиков происходит в дескрипторе teq-плагина (teqfw.json):
{
}
}
}
}
/** @type {TeqFw_Web_Back_Model_Address} */
const mAddress = spec[‘TeqFw_Web_Back_Model_Address$’];
/** @type {TeqFw_Web_Back_Server_Respond.respond405|function} */
const respond405 = spec[‘TeqFw_Web_Back_Server_Respond.respond405’];
/** @type {TeqFw_Web_Back_Api_Request_IHandler[]} */
const handlers = [];
async function onRequest(req, res) {
function isMethodAllowed(method) { }
async function parseBody(method, headers, req) { } const {headers, method, url} = req;
if (isMethodAllowed(method)) {
await parseBody(method, headers, req);
const address = mAddress.parsePath(url);
// collect processors
const active = [];
for (const one of handlers)
if (one.requestIsMine({method, address, headers}))
active.push(one.getProcessor());
// run processors one by one
for (const one of active)
await one(req, res);
} else respond405(res);
} В общем случае обработчик не должен сам отправлять ответ — ему неизвестно, какие ещё обработчики есть в очереди после него. Так как результатом обработки запроса является сообщение, состоящее из заголовков и тела запроса, то каждый обработчик может добавить свои заголовки в res (через response.setHeader(name, value)), а тело ответа или имя файла для отправки клиенту сохранить в res[‘teqBody’] или в res[‘teqFile’] соответственно. Если обработчик считает, что его данных достаточно для формирования ответа, он добавляет код ответа в res[‘teqStatus’].
function process(req, res) {
if (!res.headersSent) {
const headers = res.getHeaders();
const statusCode = res[DEF.RES_STATUS] ?? HTTP_STATUS_OK;
const file = res[DEF.RES_FILE];
const body = res[DEF.RES_BODY];
let stat;
if (file) {
if (
existsSync(file)
) {
// …
const readStream = createReadStream(file);
res.writeHead(statusCode, headers);
pipeline(readStream, res);
}
} else if (body) {
res.writeHead(statusCode, headers);
res.end(body);
} else respond404(res);
}
}
{
}
}
}
}
/** @type {TeqFw_Web_Back_Model_Address} */
const mAddress = await container.get(‘TeqFw_Web_Back_Model_Address$’);
//…
const address = mAddress.parsePath(url);
if (address?.space === ‘api’) { /* … */}
{
“path”: “./sources”
}
}
}
{
“/vue/”: “/vue/dist/”
}
}
}
./node_modules/@vnd/pkg/web/css/style.css
{
“TeqFw_Web_Back_WAPI_Load_Namespaces”
]
}
}
{
}
}
}