В старом коде Битрикс до сих пор встречается прямой доступ к $_GET, $_POST, $_SERVER и header(). На простом скрипте это «работает», на проде ломается: CLI-агент падает без HTTP-контекста, JSON-тело не попадает в $_POST, редирект уходит после уже отправленного вывода, а тесты невозможно прогнать без подмены суперглобалов. Нормальный контракт в D7 — связка BitrixMainApplication → Context → HttpRequest / 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 redirect —
LocalRedirect($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 в долгоживущий сервис как состояние — передавайте нужные поля аргументами.
Краткий чеклист
- Входные данные — только через
HttpRequest(или аргументы action). - JSON —
getInput()+Json::decode, не$_POST. - Сайт/язык — через
Context, если важна многосайтовость. - Ответ API — явный статус,
Content-Type, единый JSON-контракт. - Редиректы — без open redirect; после вывода не слать Location.
- CLI/агенты — не полагаться на HTTP Request.
Заключение
Application / Context / Request / Response — базовый каркас любого кода, который принимает HTTP в Битрикс D7. Если модуль до сих пор сидит на суперглобалах, начните с замены чтения параметров и JSON-тела: это сразу убирает пустой $_POST, упрощает контроллеры и делает поведение одинаковым в компоненте, ajax-скрипте и Engine action. Дальше по серии логично разобрать ORM DataManager или агенты — туда же обычно уходит уже проверенный payload из Request.
