Bitrix D7 Application, Context, Request и Response: HTTP-контекст модуля

В старом коде Битрикс до сих пор встречается прямой доступ к $_GET, $_POST, $_SERVER и header(). На простом скрипте это «работает», на проде ломается: CLI-агент падает без HTTP-контекста, JSON-тело не попадает в $_POST, редирект уходит после уже отправленного вывода, а тесты невозможно прогнать без подмены суперглобалов. Нормальный контракт в D7 — связка BitrixMainApplicationContextHttpRequest / HttpResponse.

Ниже — практический разбор: как получить приложение и контекст, читать query/post/заголовки/JSON, отдавать ответ и редирект, чем это отличается от $_REQUEST, какие ошибки ловят чаще всего. Связка с серией D7: исходящие HTTP-вызовы — через HttpClient; сервисы модуля — через ServiceLocator; результат операции — через Result / Error.

Что такое Application, Context, Request и Response

BitrixMainApplication — синглтон ядра: точка входа к соединению с БД, кешу, документруту и текущему контексту выполнения. BitrixMainContext (часто HttpContext на вебе) держит окружение одного запроса: сайт, язык, культура, сервер, запрос и ответ. BitrixMainHttpRequest — обёртка над входящими данными. BitrixMainHttpResponse — исходящие заголовки, статус, тело и редиректы.

Типичная цепочка в модуле, компоненте или контроллере:

use BitrixMainApplication;

$app = Application::getInstance();
$context = $app->getContext();
$request = $context->getRequest();
$response = $context->getResponse();

// чаще пишут короче:
$request = Application::getInstance()->getContext()->getRequest();

На CLI (агент, консольная команда, cron-скрипт) HTTP-контекст может быть «пустым» или урезанным: нет реального URI, нет cookie браузера. Не читайте $_SERVER['REQUEST_URI'] в агенте — берите параметры из аргументов агента или из очереди, а не из Request.

Application: что берут чаще всего

Помимо контекста, у приложения есть методы, которыми пользуются в каждом втором модуле:

use BitrixMainApplication;

$app = Application::getInstance();

$connection = $app->getConnection();           // BitrixMainDBConnection
$docRoot = $app->getDocumentRoot();            // DOCUMENT_ROOT без хвоста /
$managedCache = $app->getManagedCache();       // управляемый кеш
$taggedCache = $app->getTaggedCache();         // тегированный кеш

// аварийное завершение после заголовков/ответа
$app->end(); // или terminate() в зависимости от версии/обёртки

Соединение с БД через getConnection() — правильный путь вместо глобального $DB. Кеш — тема отдельной статьи про Cache и TaggedCache; здесь важно лишь то, что Application — фасад, а не место для бизнес-логики.

Context: сайт, язык, сервер

Контекст нужен, когда поведение зависит от сайта или локали, а не только от query-параметров:

use BitrixMainApplication;

$context = Application::getInstance()->getContext();

$siteId = $context->getSite();           // например "s1"
$langId = $context->getLanguage();       // "ru"
$culture = $context->getCulture();       // форматы даты/чисел
$server = $context->getServer();         // Server — обёртка над $_SERVER

$host = $server->getHttpHost();
$remoteAddr = $server->getRemoteAddr();
$isHttps = $server->getRequestScheme() === 'https'
    || $server->get('HTTPS') === 'on';

Не смешивайте SITE_ID из константы и $context->getSite() без необходимости: в агентах и при смене сайта константа может врать относительно текущего контекста. Для многосайтовости проверяйте сайт явно.

HttpRequest: query, post, файлы, заголовки

Базовое чтение параметров — через методы Request, а не через суперглобалы:

use BitrixMainApplication;

$request = Application::getInstance()->getContext()->getRequest();

$id = (int)$request->get('id');                 // GET или POST (как $_REQUEST, но контролируемо)
$name = (string)$request->getPost('name');      // только POST
$code = (string)$request->getQuery('code');     // только GET
$token = (string)$request->getCookie('MY_TOKEN');

$isPost = $request->isPost();
$isAjax = $request->isAjaxRequest();
$method = $request->getRequestMethod();         // GET, POST, ...

$page = $request->getRequestedPage();           // физическая страница
$uri = $request->getRequestUri();               // URI с query
$script = $request->getScriptName();

Заголовки (Authorization, X-Requested-With, Content-Type) читайте так:

$contentType = (string)$request->getHeader('Content-Type');
$auth = (string)$request->getHeader('Authorization');
$xRequested = (string)$request->getHeader('X-Requested-With');

Имена заголовков в D7 обычно нормализуются; не рассчитывайте на точный регистр как в сыром $_SERVER['HTTP_...']. Для API с Bearer-токеном всегда берите заголовок через Request и валидируйте отдельно — не кладите секрет в query.

JSON-тело и php://input

Классическая ловушка: фронт шлёт Content-Type: application/json, а разработчик читает $_POST — массив пустой. В D7 тело читают так:

use BitrixMainApplication;
use BitrixMainWebJson;
use BitrixMainSystemException;

$request = Application::getInstance()->getContext()->getRequest();

$raw = $request->getInput(); // сырое тело
$data = [];

if ($raw !== '' && $raw !== false) {
    try {
        $data = Json::decode($raw);
    } catch (SystemException $e) {
        // невалидный JSON
        $data = [];
    }
}

// если ждёте объект/ассоциативный массив:
$items = is_array($data['items'] ?? null) ? $data['items'] : [];

В контроллерах Engine (BitrixMainEngineController) JSON часто разбирается фильтрами и аргументами action — но в обычном компоненте, ajax.php модуля или кастомном endpoint используйте getInput() + Json::decode. Не вызывайте file_get_contents('php://input') второй раз: поток одноразовый, Request уже мог его прочитать.

HttpResponse: статус, заголовки, JSON, редирект

Ответ тоже лучше отдавать через D7, особенно если дальше подключены буферы ядра или модули, которые дописывают заголовки:

use BitrixMainApplication;
use BitrixMainWebJson;

$app = Application::getInstance();
$response = $app->getContext()->getResponse();

$response->addHeader('Content-Type', 'application/json; charset=UTF-8');
$response->setStatus('200 OK');
$response->setContent(Json::encode([
    'success' => true,
    'data' => ['id' => 42],
]));

$response->writeHeaders();
echo $response->getContent();
$app->end();

Редирект:

use BitrixMainApplication;

$request = Application::getInstance()->getContext()->getRequest();
$response = Application::getInstance()->getContext()->getResponse();

$back = $request->get('back');
// никогда не редиректите на произвольный внешний URL из query без whitelist
if (is_string($back) && str_starts_with($back, '/')) {
    LocalRedirect($back); // привычный хелпер Битрикс
    // либо через response->redirect(...) в актуальной ветке ядра
}

LocalRedirect по-прежнему распространён и нормален для публички. Главное — не строить open redirect из необработанного параметра и не слать header('Location: ...') после того, как компонент уже что-то вывел в буфер.

Практический пример: ajax-endpoint модуля

Минимальный обработчик в /local/modules/mymodule/ajax/save.php (подключение через urlrewrite или прямой путь), без Engine Controller:

<?php
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use BitrixMainApplication;
use BitrixMainLoader;
use BitrixMainWebJson;
use BitrixMainSystemException;

header('Content-Type: application/json; charset=UTF-8');

if (!Loader::includeModule('mymodule')) {
    http_response_code(500);
    echo Json::encode(['success' => false, 'error' => 'MODULE']);
    require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php';
    die();
}

$app = Application::getInstance();
$request = $app->getContext()->getRequest();

if (!$request->isPost()) {
    http_response_code(405);
    echo Json::encode(['success' => false, 'error' => 'METHOD']);
    require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php';
    die();
}

global $USER;
if (!$USER->IsAuthorized()) {
    http_response_code(401);
    echo Json::encode(['success' => false, 'error' => 'AUTH']);
    require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php';
    die();
}

$raw = (string)$request->getInput();
try {
    $payload = $raw !== '' ? Json::decode($raw) : [];
} catch (SystemException $e) {
    http_response_code(400);
    echo Json::encode(['success' => false, 'error' => 'JSON']);
    require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php';
    die();
}

$id = (int)($payload['id'] ?? $request->getPost('id') ?? 0);
$title = trim((string)($payload['title'] ?? ''));

if ($id <= 0 || $title === '') {
    http_response_code(422);
    echo Json::encode(['success' => false, 'error' => 'VALIDATION']);
    require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php';
    die();
}

// ... сохранение через ORM / API модуля ...

echo Json::encode(['success' => true, 'data' => ['id' => $id]]);
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php';

Для новых модулей предпочтительнее Engine Controller + routing (local/routes) — меньше ручного JSON и статусов. Но понимание Request/Response всё равно нужно: фильтры контроллера опираются на тот же HTTP-контекст.

Типичные ошибки

  • Читать $_POST при JSON — тело пустое. Используйте getInput() + Json::decode.
  • Писать в $_GET/$_POST «для удобства» — ломаете отладку и безопасность. Передавайте аргументы явно.
  • Использовать Request в агенте как источник правды — у агента нет пользовательского HTTP-запроса. Параметры агента задаются при регистрации.
  • Open redirectLocalRedirect($request->get('back')) на внешний URL. Разрешайте только относительные пути своего сайта или whitelist доменов.
  • Двойное чтение php://input — второй раз будет пустая строка. Один раз через Request.
  • Путать get(), getQuery() и getPost() — для CSRF-чувствительных операций берите только POST и проверяйте sessid / свой токен.
  • Отдавать HTML-ошибку вместо JSON в API — фронт падает на JSON.parse. Держите единый формат ответа и корректный Content-Type.
  • Игнорировать кодировку и XSS — значения из Request при выводе в HTML экранируйте (htmlspecialcharsbx), в SQL не конкатенируйте — ORM/плейсхолдеры.

Связка с контроллерами и событиями

В Engine Controller текущий запрос доступен как $this->getRequest(). Pre-фильтры (HttpMethod, ContentType, Authentication, Csrf) как раз валидируют HTTP-контекст до action. Подписки на ядро по-прежнему идут через EventManager: в обработчике при необходимости снова берите Application::getInstance()->getContext()->getRequest(), но не тяните Request в долгоживущий сервис как состояние — передавайте нужные поля аргументами.

Краткий чеклист

  1. Входные данные — только через HttpRequest (или аргументы action).
  2. JSON — getInput() + Json::decode, не $_POST.
  3. Сайт/язык — через Context, если важна многосайтовость.
  4. Ответ API — явный статус, Content-Type, единый JSON-контракт.
  5. Редиректы — без open redirect; после вывода не слать Location.
  6. CLI/агенты — не полагаться на HTTP Request.

Заключение

Application / Context / Request / Response — базовый каркас любого кода, который принимает HTTP в Битрикс D7. Если модуль до сих пор сидит на суперглобалах, начните с замены чтения параметров и JSON-тела: это сразу убирает пустой $_POST, упрощает контроллеры и делает поведение одинаковым в компоненте, ajax-скрипте и Engine action. Дальше по серии логично разобрать ORM DataManager или агенты — туда же обычно уходит уже проверенный payload из Request.