---
title: "TeqFW: Web server"
description: "Web-сервер на [платформе TeqFW](https://wiredgeese.com/%D1%87%D1%82%D0%BE-%D1%82%D0%B0%D0%BA%D0%BE%D0%B5-teqfw-208d5938205) может работать в одном из трёх режимов: HTTP/1, HTTP/2 и"
date: 2021-12-15
---

Web-сервер на [платформе
TeqFW](https://wiredgeese.com/%D1%87%D1%82%D0%BE-%D1%82%D0%B0%D0%BA%D0%BE%D0%B5-teqfw-208d5938205)
может работать в одном из трёх режимов: HTTP/1, HTTP/2 и HTTPS (over
HTTP/2). Первый режим (HTTP/1) применяется для разработки — запуск
приложения локально и доступ к нему из браузера напрямую, второй — для
работы в связке с другими серверами (например, доступ через
nginx-прокси), третий — в качестве отдельного сервера в live-режиме.

## Основы HTTP

HTTP всегда работает по схеме “*запрос* — *ответ*”. Вначале отправляется
стартовая строка с адресом ресурса на сервере, за ней следуют
HTTP-заголовки, потом — тело запроса (опционально). При ответе сервер
сначала посылает HTTP-заголовки, а затем — тело ответа. Тело ответа
может быть растянуто во времени (как в [Server Sent
Events](https://en.wikipedia.org/wiki/Server-sent_events) — заголовки,
событие, событие, …).

В nodejs есть две библиотеки для запуска web-сервера:
[http](https://nodejs.org/api/http.html) и
[http2](https://nodejs.org/api/http2.html). Сервера в обеих библиотеках
построены по [общему
принципу](https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/)
— на каждый HTTP-запрос генерируется ряд событий, на которые можно
повесить собственные функции-обработчики (*listeners*, “*прослушки*”):

```js
server.on('connect', onConnect);
server.on('error', onError);
server.on('request', onRequest);
server.on('close', onClose);
```

… HTTP-сервера на каждый запрос формируют структуры для работы с
входными и выходными данными и предоставляют разработчику возможность
прослушивать событие `request` для доступа к ним:

```js
server.on('request', (req, res) => {});
```

Структуры req и res являются также потоками, за чтение/запись данных в которые отвечает прослушка (listener), т.е. — разработчик приложения.

Важным является то, что сообщения в HTTP состоят из двух частей:
заголовков и тела. Все заголовки всегда отправляются перед телом (после
начала отправки тела запроса/ответа заголовки отправлять более нельзя).

## Основы обработки запроса в TeqFW

Прослушка события `request` выполняет предварительный анализ
поступившего запроса, оставляя только те запросы, которые соответствуют
допустимым HTTP-методам (для TeqFW — HEAD, GET & POST). На все другие
запросы прослушка возвращает код 405 ([Method Not
Allowed](https://developer.mozilla.org/ru/docs/Web/HTTP/Status/405)).

Для POST-запросов, у которых `Content-Type` начинается с `text/plain`
или с `application/json`, прослушка считывает тело запроса и преобразует
его в текст или в JSON-объект, который затем прикрепляется к структуре
`req`. Во всех остальных случаях прослушка входной поток не трогает.

Разработчики `teq`-плагинов могут создавать собственные обработчики
web-запросов, которые `request`-прослушка использует для обработки
поступившего запроса и формирования ответа. Обработчики по очереди
анализируют запрос и, если запрос находится в их зоне ответственности,
выполняют соответствующие действия. Если в результате этих действий
формируются HTTP-заголовки для их отправки клиенту, то они добавляются к
структуре `res` (через `response.setHeader(name, value)`). Если
обработчик считает, что ответом должно быть содержимое некоего файла, то
он указывает имя файла в `res[‘teqFile’]`, если ответом должен быть
текст, то он указывается в `res[‘teqBody’]`.

Последним `request`-прослушка запускает обработчик
`TeqFw_Web_Back_Handler_Final`, который анализирует `res`-структуру и
формирует ответ (сначала все заголовки, затем отдаёт файл или текст тела
сообщения). Если ни один из обработчиков запрос не обработал, то
финальный обработчик возвращает код 404 ([Not
Found](https://developer.mozilla.org/ru/docs/Web/HTTP/Status/404)).

Если какой-то обработчик считает, что он должен сам полностью ответить
на сообщение, то он может это сделать. Другие обработчики, включая
финальный, должны учитывать такую возможность.

## Диспетчер запросов

В TeqFW прослушка для `request`-события создаётся в модуле
`TeqFw_Web_Back_Server_Dispatcher` (метод `getListener()`). Перед
подключением прослушки Диспетчер сканирует все плагины в поисках
обработчиков web-запросов и инициализирует их:

```js
await dispatcher.createHandlers();
const onRequest = dispatcher.getListener();
server.on('request', onRequest);
```

## Обработчики запросов

Обработчик web-запросов — это объект, имплементирующий интерфейс
`TeqFw_Web_Back_Api_Request_IHandler`:

```js
class TeqFw_Web_Back_Api_Request_IHandler {
  getProcessor() {}
  async init() {}
  requestIsMine({method, address, headers} = {}) {}
}
```

Метод init() предназначен для асинхронной инициализации обработчика Диспетчером, метод requestIsMine() сообщает Диспетчеру, что обработчик признаёт запрос “своим”, метод getProcessor() возвращает Диспетчеру функцию — процессор запроса:

```js
/**
 * @param {http.IncomingMessage|http2.Http2ServerRequest} req
 * @param {http.ServerResponse|http2.Http2ServerResponse} res
 */
function process(req, res) {}
```

Подключение обработчиков происходит в дескрипторе teq-плагина (teqfw.json):
```json
{
  "@teqfw/web": {
    "handlers": {
      "TeqFw_Web_Back_Handler_Upload": {
        "after": ["TeqFw_Web_Back_Handler_WAPI"],
        "before": ["TeqFw_Web_Back_Handler_Static"],
        "space": ["upload"]
      }
    }
  }
}
```

Порядок подключения обработчиков определяется на основании данных в
узлах `after` и `before`. Узел `space` позволяет обработчику
зарезервировать адресное пространство для “своих” запросов (см.
“[Адресация
ресурсов](https://wiredgeese.com/teqfw-%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D1%8B-ffdf439a1079#1469)”).

## Обработка запроса Диспетчером

При обработке HTTP-запроса Диспетчер проверяет метод запроса (HEAD, GET,
POST) и считывает тело запроса (если тип содержимого `text/plain` или
`application/json`). После чего проверяет очередь обработчиков и
выбирает те, кто готов обработать запрос основываясь на данных
заголовков и адресе запроса. Отобранные обработчики запускаются
по-очереди в асинхронном режиме:

```js
/** @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’].

Таким образом, каждый обработчик из очереди может поучаствовать в
обработке запроса, добавить какие-либо данные в `req`-структуру (будут
доступны для следующих обработчиков) или посмотреть в `res`-структуре,
был ли запрос обработан предыдущими обработчиками и с каким результатом.

## Финальный обработчик

В общем случае за формирование ответа на запрос отвечает финальный
обработчик — `TeqFw_Web_Back_Handler_Final`. Он проверяет, что запрос не
был обработан полностью каким-либо обработчиком (`res.headersSent`),
смотрит, что нужно отправить в качестве ответа (файл или текст),
формирует и отправляет ответ.

```js
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) && (stat = statSync(file)) && stat.isFile()) {
        // …
        const readStream = createReadStream(file);
        res.writeHead(statusCode, headers);
        pipeline(readStream, res);
      }
    } else if (body) {
      res.writeHead(statusCode, headers);
      res.end(body);
    } else {
      respond404(res);
    }
  }
}
```

Если запрос вообще не был обработан ни одним обработчиком, то финальный
обработчик отправляет клиенту ответ со статусом 404 ([Not
Found](https://developer.mozilla.org/ru/docs/Web/HTTP/Status/404)).

## Доступные обработчики

На момент написания публикации в плагине `@teqfw/web` определены
следующие обработчики:

- `TeqFw_Web_Back_Handler_SSE`: для использования [Server Sent
  Events](https://ru.wikipedia.org/wiki/Server-sent_events);
- `TeqFw_Web_Back_Handler_WAPI`: Web API, позволяющий клиенту получать
  JSON-данные через GET-запросы или обмениваться с сервером JSON-данными
  через POST-запросы;
- `TeqFw_Web_Back_Handler_Upload`: загрузка файлов на сервер;
- `TeqFw_Web_Back_Handler_Static`: раздача статики;
- `TeqFw_Web_Back_Handler_Final`: финальный обработчик;

Обработчики задают [пространства
адресов](https://wiredgeese.com/teqfw-%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D1%8B-ffdf439a1079#1469),
очерчивая свою “зону ответственности” (`sse`, `api`, `upload`, `src` и
`web`):

```json
{
  "@teqfw/web": {
    "handlers": {
      "TeqFw_Web_Back_Handler_Static": {
        "spaces": ["src", "web"]
      }
    }
  }
}
```

Модель адреса в платформе (`TeqFw_Web_Back_Model_Address`) способна
определять, к какому пространству относится запрашиваемый ресурс:

```js
/** @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') { /* … */ }
```

## Раздача статики

За раздачу статики отвечает обработчик `TeqFw_Web_Back_Handler_Static`.
Вся статика делится на две группы:

- пространство `src`: исходные коды `npm`-пакетов из `./node_modules/`;
- пространство `web`: произвольные файлы из каталогов
  `./web/` `teq`-плагинов;

### Пространство `src`

Каждый `teq`-плагин прописывает в дескрипторе `./teqfw.json` настройки
для автозагрузки кода [контейнером
объектов](https://wiredgeese.com/%D0%B2%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B5%D0%B9-%D0%B2-teqfw-b1beb319ca56).
Например, для `teq`-плагина с именем `@vnd/pkg` указана такая настройка
автозагрузки:

```json
{
  "@teqfw/di": {
    "autoload": {
      "ns": "Vnd_Pkg",
      "path": "./sources"
    }
  }
}
```

Обработчик статики использует эту настройку, чтобы в пространстве
`./src/` предоставлять доступ к исходникам. Например, адрес:

`https://host.com/src/@vnd/pkg/Path/To/Module.mjs`
соответствует такому пути к файлу внутри проекта:

`./node_modules/@vnd/pkg/sources/Path/To/Module.mjs`

Если `npm`-пакет не является `teq`-плагином (нет дескриптора
`./teqfw.json` в корне пакета), то для таких пакетов маппинг исходников
можно указать вручную в дескрипторе того плагина, который использует
этот `npm`-пакет. Вот, например, маппинг статики для плагина
`@teqfw/vue` :

```json
{
  "@teqfw/web": {
    "statics": {
      "/vue-router/": "/vue-router/dist/",
      "/vue/": "/vue/dist/"
    }
  }
}
```

Таким образом, подобный маппинг позволяет перенаправить все адреса:

`https://host.com/src/vue/…` на файловую систему:

`./node_modules/vue/dist/…`

### Пространство `web`

Каждый `teq`-плагин может иметь в корне каталог `./web/`, всё содержимое
которого может быть доступным через web-сервер. Обработчик статики при
инициализации сканирует все `teq`-плагины и составляет карту для доступа
к таким ресурсам. Так, адрес:

`https://host.com/web/@vnd/pkg/css/style.css`
соответствует такому пути к файлу внутри проекта:

`./node_modules/@vnd/pkg/web/css/style.css`

## Web API (сервисы)

Обработчик `TeqFw_Web_Back_Handler_WAPI` отвечает за пространство
`./api/` и реагирует на `GET`&`POST` методы. Если `POST`-запрос содержит
какие-то данные, то ожидается, что это будет валидный JSON.

Сервисы внутри `api`-пространства адресуются маршрутами (*routes*,
*endpoints*) без учёта метода запроса (один и тот же сервис и для `GET`,
и для `POST`). Каждый `teq`-плагин определяет собственное
подпространство в соответствии со своим `npm`-именем и уже внутри
собственного подпространства определяет имя маршрута, по которому
располагается соответствующий `wapi`-сервис:

`http://localhost/api/@teqfw/web/load/namespaces`

Результатом работы сервиса также является JSON.

`WAPI`-сервисы teq-плагина регистрируются в его дескрипторе:

```json
{
  "@teqfw/web": {
    "wapi": [
      "TeqFw_Web_Back_WAPI_Load_Config",
      "TeqFw_Web_Back_WAPI_Load_FilesToCache",
      "TeqFw_Web_Back_WAPI_Load_Namespaces"
    ]
  }
}
```

Подробное описание данного обработчика выходит за рамки этой статьи и
требует отдельной публикации.

## Server Sent Events и Files Upload

Эти обработчики принципиально реализованы, но на данной стадии создавать
описание было бы преждевременным. В SSE непонятен механизм группового
использования канала связи с клиентом (когда различные `teq`-плагины
пишут в канал свои события) — сейчас возможен только один сервис на
канал. А в `upload`-обработчике пока непонятно, каким образом, без
аутентификации и авторизации, обеспечить ограниченный доступ к загрузке
файлов на сервер.

Поэтому для отключения обработчиков, которые не предполагается
использовать в приложении, в дескрипторе `./teqfw.json` приложения можно
прописать:

```json
{
  "@teqfw/web": {
    "excludes": {
      "handlers": ["TeqFw_Web_Back_Handler_Upload"]
    }
  }
}
```

## Резюме

Web-сервер платформы TeqFW может запускаться в одном из трёх режимов:

- HTTP/1
- HTTP/2
- HTTPS

В нём реализованы обработка:

- статики;
- запросы к сервисам Web API;
- загрузка файлов на сервер;
- генерация событий сервером (Server Sent Events);

При необходимости возможно добавление дополнительных обработчиков
(аутентификация, логирование и т.п.).
