Запросы
Эта страница переведена с английского автоматически. Читать оригинал
Запросы — это структуры, представляющие HTTP‑сообщение запроса. Объект HttpRequest содержит полезные функции для обработки HTTP‑сообщений во всём вашем приложении.
HTTP‑запрос состоит из метода, пути, версии, заголовков и тела.
В этом документе мы покажем, как получить каждый из этих элементов.
Получение метода запроса #
Чтобы получить метод полученного запроса, можно использовать свойство Method:
static HttpResponse Index(HttpRequest request)
{
HttpMethod requestMethod = request.Method;
...
}Это свойство возвращает метод запроса, представленный объектом HttpMethod.
Примечание
В отличие от методов маршрута, это свойство не обслуживает элемент RouteMethod.Any. Вместо этого оно возвращает реальный метод запроса.
Получение компонентов URL запроса #
Вы можете получить различные компоненты URL через определённые свойства запроса. Для примера рассмотрим URL:
http://localhost:5000/user/[email protected]| Имя компонента | Описание | Значение компонента |
|---|---|---|
| Path | Получает путь запроса. | /user/login |
| FullPath | Получает путь запроса и строку запроса. | /user/[email protected] |
| FullUrl | Получает полную строку URL запроса. | http://localhost:5000/user/[email protected] |
| Host | Получает хост запроса. | localhost |
| Authority | Получает хост и порт запроса. | localhost:5000 |
| QueryString | Получает строку запроса. | [email protected] |
| Query | Получает запрос в виде коллекции именованных значений. | {StringValueCollection object} |
| IsSecure | Определяет, использует ли запрос SSL (true) или нет (false). | false |
Вы также можете воспользоваться свойством HttpRequest.Uri, которое включает всё перечисленное в одном объекте.
Метаданные запроса и отмена #
Sisk также добавляет к каждому запросу оперативные метаданные. Эти свойства полезны для журналов, трассировки, локализации, диагностики и длительных операций:
| Свойство или метод | Назначение |
|---|---|
| RequestId | Уникальный идентификатор запроса. Включите IncludeRequestIdHeader, чтобы возвращать его как X-Request-Id. |
| RequestedAt | Момент, когда Sisk создал объект запроса. |
| RemoteAddress | Адрес клиента, полученный из соединения, или из вашего ForwardingResolver. |
| Culture | Наиболее подходящая культура, определённая из Accept-Language, с откатом к текущей культуре. |
| DisconnectToken | Токен отмены, который сигнализируется при отключении клиента, если поддерживается настроенным HTTP‑движком. |
| Bag | Типизированное хранилище ключ/значение, совместно используемое обработчиками запросов и действием маршрута. |
| GetRawHttpRequest | Текстовое представление запроса для диагностики. |
Получение тела запроса #
Некоторые запросы содержат тело, например формы, файлы или API‑транзакции. Тело запроса можно получить через свойство:
// получает тело запроса как строку, используя кодировку запроса в качестве кодировщика
string body = request.Body;
// или получает его в виде массива байтов
byte[] bodyBytes = request.RawBody;
// либо можно получить поток.
Stream requestStream = request.GetRequestStream();
// или прочитать тело асинхронно
Memory<byte> bodyMemory = await request.GetBodyContentsAsync();Также можно определить, есть ли тело в запросе и загружено ли оно, с помощью свойств HasContents, которое определяет наличие содержимого, и IsContentAvailable, которое указывает, что HTTP‑сервер полностью получил содержимое от удалённого источника.
Невозможно прочитать содержимое запроса через GetRequestStream более одного раза. Если вы читаете с помощью этого метода, значения в RawBody и Body также будут недоступны. Не требуется освобождать поток запроса в контексте запроса, так как он освобождается в конце HTTP‑сессии, в которой был создан. Кроме того, вы можете использовать свойство HttpRequest.RequestEncoding, чтобы получить оптимальную кодировку для ручного декодирования запроса.
Сервер накладывает ограничения на чтение содержимого запроса, которые применяются как к HttpRequest.Body, так и к HttpRequest.RawBody. Эти свойства копируют весь входной поток в локальный буфер размером, равным HttpRequest.ContentLength.
Содержимое запроса ограничивается параметром HttpServerConfiguration.MaximumContentLength, который по умолчанию составляет 100 МБ начиная с Sisk 1.7 (в более ранних версиях было 0, без ограничений). Ограничение применяется следующим образом:
- Запрос, указывающий
Content-Length, превышающий лимит, получает ответ413 Content Too Largeдо вызова маршрута. - Тело, передаваемое кусками или в виде потока без объявленной длины, проверяется во время чтения. Чтение за пределы лимита через RawBody, Body или поток, возвращаемый GetRequestStream, вызывает InsufficientMemoryException, что приводит к ошибке
500 Internal Server Error, если ваш код или Router.CallbackErrorHandler не обработают её. - То же ограничение является максимальным размером получаемого сообщения WebSocket.
Установите значение свойства в 0, чтобы снять ограничение:
using var app = HttpServer.CreateBuilder()
.UseConfiguration(config => config.MaximumContentLength = 0)
.Build();Даже без установленного ограничения Body и RawBody не могут буферизовать содержимое, превышающее Array.MaxLength (около 2 ГБ), и бросают исключение при попытке доступа к ним. По‑прежнему можно работать с большим содержимым через потоковую передачу.
Примечание
Хотя Sisk позволяет это, всегда рекомендуется следовать семантике HTTP при создании приложения и не получать или обслуживать содержимое в методах, которые этого не допускают. Подробнее см. RFC 9110 “HTTP Semantics”.
Чтение JSON‑запросов #
Для JSON‑API предпочтительно использовать встроенные помощники JSON вместо чтения Body и ручной десериализации. Они используют System.Text.Json и по умолчанию применяют HttpRequest.DefaultJsonSerializerOptions.
public record CreateUserRequest(string Name, string Email);
router.MapPost("/users", (HttpRequest request) =>
{
CreateUserRequest? body = request.GetJsonContent<CreateUserRequest>();
if (body is null)
return new HttpResponse(System.Net.HttpStatusCode.BadRequest);
return new HttpResponse(System.Net.HttpStatusCode.Created);
});Используйте асинхронную перегрузку, когда вы уже находитесь в асинхронном маршруте или хотите, чтобы отмена запроса прерывала десериализацию:
router.MapPost("/users", async (HttpRequest request) =>
{
CreateUserRequest? body =
await request.GetJsonContentAsync<CreateUserRequest>(request.DisconnectToken);
if (body is null)
return new HttpResponse(System.Net.HttpStatusCode.BadRequest);
return new HttpResponse(System.Net.HttpStatusCode.Created);
});Для конкретной конечной точки можно передать пользовательские JsonSerializerOptions:
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true
};
UserDto? user = request.GetJsonContent<UserDto>(options);Для приложений Native AOT или чувствительных к обрезке используйте перегрузку JsonTypeInfo<T>, генерируемую JsonSerializerContext:
[JsonSerializable(typeof(CreateUserRequest))]
public partial class AppJsonSerializerContext : JsonSerializerContext
{
}
CreateUserRequest? body =
await request.GetJsonContentAsync(
AppJsonSerializerContext.Default.CreateUserRequest,
request.DisconnectToken);То же правило «прочитать один раз» применяется к JSON‑помощникам: после того как Sisk прочитает поток запроса через GetJsonContent, GetJsonContentAsync, Body или RawBody, вы не сможете позже использовать тот же тело через GetRequestStream().
Получение контекста запроса #
HTTP‑Context — это эксклюзивный объект Sisk, который хранит информацию о HTTP‑сервере, маршруте, роутере и обработчике запросов. Вы можете использовать его, чтобы упорядочить работу в среде, где эти объекты сложно организовать.
Текущий исполняемый HttpContext можно получить с помощью статического метода HttpContext.GetCurrentContext(). Этот метод возвращает контекст запроса, который в данный момент обрабатывается в текущем потоке.
HttpContext context = HttpContext.GetCurrentContext();Режим журналирования #
Свойство HttpContext.LogMode позволяет управлять поведением журналирования для текущего запроса. Вы можете включать или отключать журналирование для отдельных запросов, переопределяя конфигурацию сервера по умолчанию.
// Отключить журналирование для этого запроса
context.LogMode = LogOutputMode.None;Корзина запросов #
Объект RequestBag содержит сохранённую информацию, передаваемую от одного обработчика запроса к другому, и может быть использована в конечной точке. Этот объект также может использоваться обработчиками запросов, которые выполняются после обратного вызова маршрута.
Совет
Это свойство также доступно через свойство HttpRequest.Bag.
public class AuthenticateUserRequestHandler : IRequestHandler
{
public string Identifier { get; init; } = Guid.NewGuid().ToString();
public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse;
public HttpResponse? Execute(HttpRequest request, HttpContext context)
{
if (request.Headers.Authorization != null)
{
context.RequestBag.Add("AuthenticatedUser", new User("Bob"));
return null;
}
else
{
return new HttpResponse(System.Net.HttpStatusCode.Unauthorized);
}
}
}Приведённый выше обработчик запроса добавит AuthenticatedUser в корзину запросов, и его можно будет использовать позже в финальном обратном вызове:
public class MyController
{
[RouteGet("/")]
[RequestHandler<AuthenticateUserRequestHandler>]
static HttpResponse Index(HttpRequest request)
{
User authUser = request.Context.RequestBag["AuthenticatedUser"];
return new HttpResponse() {
Content = new StringContent($"Hello, {authUser.Name}!")
};
}
}Вы также можете использовать вспомогательные методы Bag.Set() и Bag.Get() для получения или установки объектов по их типу‑синглтону.
Класс TypedValueDictionary также предоставляет методы GetValue и SetValue для более гибкого управления.
public class Authenticate : RequestHandler
{
public override HttpResponse? Execute(HttpRequest request, HttpContext context)
{
request.Bag.Set<User>(authUser);
}
}[RouteGet("/")]
[RequestHandler<Authenticate>]
public static HttpResponse GetUser(HttpRequest request)
{
var user = request.Bag.Get<User>();
...
}Атомарные значения контекста #
Корзина запросов не синхронизирована. Когда числовые значения в корзине могут изменяться конкурентным кодом в рамках одного запроса, например параллельными задачами, запущенными маршрутом, используйте HttpContext.Interlocked. Он возвращает объект HttpContextInterlocked с атомарными операциями над именованными значениями корзины запросов:
| Метод | Описание |
|---|---|
Add(name, value) | Добавляет value к сохранённому значению, или сохраняет value, если name не установлено. |
Exchange(name, value) | Заменяет сохранённое значение и возвращает предыдущее, или value, если name не было установлено. |
CompareExchange(name, value, comparand, notFound = -1) | Заменяет сохранённое значение на value только если оно равно comparand, и возвращает оригинальное значение, или notFound, если name не установлено. |
Inspect(name) | Возвращает сохранённое значение, или null, если name не установлено. |
Значения в корзине запросов хранятся как double. Перегрузки, принимающие int или long, преобразуют их в double.
router.MapPost("/import", async (HttpRequest request) =>
{
string[] lines = request.Body.Split('\n');
await Parallel.ForEachAsync(lines, async (line, cancellation) =>
{
if (await TryImportAsync(line, cancellation))
request.Context.Interlocked.Add("imported", 1);
});
double imported = request.Context.Interlocked.Inspect("imported") ?? 0;
return new HttpResponse($"{imported} line(s) imported");
});Примечание
Операции атомарны только относительно других вызовов Interlocked. Чтение или запись того же ключа напрямую через RequestBag не синхронизированы с ними.
Получение данных формы #
Вы можете получить значения данных формы в объекте StringKeyStoreCollection с помощью примера ниже:
[RoutePost("/auth")]
public HttpResponse Index(HttpRequest request)
{
var form = request.GetFormContent();
string? username = form["username"];
string? password = form["password"];
if (AttempLogin(username, password))
{
...
}
}Асинхронная версия полезна, когда тело запроса может быть большим или когда требуется поддержка отмены:
var form = await request.GetFormContentAsync(request.DisconnectToken);Получение multipart‑данных формы #
HTTP‑запрос Sisk позволяет получать загруженные multipart‑содержимое, такое как файлы, поля формы или любой двоичный контент.
[RoutePost("/upload-contents")]
public HttpResponse Index(HttpRequest request)
{
// следующий метод читает весь ввод запроса в
// массив объектов MultipartObject
var multipartFormDataObjects = request.GetMultipartFormContent();
foreach (MultipartObject uploadedObject in multipartFormDataObjects)
{
// Имя файла, предоставленное multipart‑данными формы.
// Возвращается null, если объект не является файлом.
Console.WriteLine("File name : " + uploadedObject.Filename);
// Имя поля объекта multipart‑данных формы.
Console.WriteLine("Field name : " + uploadedObject.Name);
// Длина содержимого multipart‑данных формы.
Console.WriteLine("Content length : " + uploadedObject.ContentLength);
// Определяет формат изображения по заголовку файла для каждого
// известного типа содержимого. Если содержимое не является распознанным общим файлом
// форматом, этот метод ниже вернёт MultipartObjectCommonFormat.Unknown
Console.WriteLine("Common format : " + uploadedObject.GetCommonFileFormat());
}
}Используйте GetMultipartFormContentAsync, когда маршрут асинхронный:
var multipartFormDataObjects =
await request.GetMultipartFormContentAsync(request.DisconnectToken);Подробнее о Multipart‑объектах формы Sisk, их методах, свойствах и возможностях, можно узнать в документации.
Обнаружение отключения клиента #
Начиная с версии v1.15 Sisk, фреймворк предоставляет токен отмены через HttpRequest.DisconnectToken. Когда настроенный HTTP‑движок поддерживает обнаружение отключения, этот токен отменяется, если соединение клиента закрывается до завершения ответа. Это полезно для остановки длительных операций, когда клиент больше не ждёт результата.
router.MapGet("/connect", async (HttpRequest req) =>
{
// gets the disconnection token from the request
var dc = req.DisconnectToken;
await LongOperationAsync(dc);
return new HttpResponse();
});Этот токен не совместим со всеми HTTP‑движками, и каждый требует собственной реализации.
Движок Sisk по умолчанию, основанный на System.Net.HttpListener, не поддерживает обнаружение отключения клиента. Когда приложение использует движок по умолчанию, DisconnectToken имеет значение CancellationToken.None; фактически это токен без возможности отмены и его следует считать недоступным.
Движок Cadente поддерживает DisconnectToken. Если ваш маршрут зависит от отмены с учётом отключения, используйте Cadente или другой движок, который явно реализует это поведение. Даже при поддерживаемом движке отмена является кооперативной: передавайте токен в асинхронные API и проверяйте его в своей длительной работе.
Поддержка Server‑sent events #
Sisk поддерживает Server‑sent events, что позволяет отправлять фрагменты как поток и поддерживать соединение между сервером и клиентом живым.
Вызов метода HttpRequest.GetEventSource переводит HttpRequest в состояние слушателя. В этом случае контекст HTTP‑запроса не будет ожидать HttpResponse, так как он будет перекрывать пакеты, отправляемые серверными событиями.
После отправки всех пакетов обратный вызов должен вернуть метод Close, который отправит окончательный ответ серверу и укажет, что поток завершён.
Невозможно предсказать общую длину всех отправляемых пакетов, поэтому определить конец соединения с помощью заголовка Content-Length невозможно.
По умолчанию большинство браузеров не поддерживают отправку HTTP‑заголовков или методов, отличных от GET, в серверных событиях. Поэтому будьте осторожны, используя обработчики запросов с event‑source, которые требуют специфических заголовков, так как, скорее всего, они не будут присутствовать.
Кроме того, большинство браузеров перезапускают потоки, если метод EventSource.close не вызывается на клиенте после получения всех пакетов, что приводит к бесконечной дополнительной обработке на стороне сервера. Чтобы избежать такой проблемы, обычно отправляют финальный пакет, указывающий, что источник событий завершил отправку всех пакетов.
Пример ниже показывает, как браузер может взаимодействовать с сервером, поддерживающим серверные события.
<html>
<body>
<b>Fruits:</b>
<ul></ul>
</body>
<script>
const evtSource = new EventSource('http://localhost:5555/event-source');
const eventList = document.querySelector('ul');
evtSource.onmessage = (e) => {
const newElement = document.createElement("li");
newElement.textContent = `message: ${e.data}`;
eventList.appendChild(newElement);
if (e.data == "Tomato") {
evtSource.close();
}
}
</script>
</html>И постепенно отправлять сообщения клиенту:
public class MyController
{
[RouteGet("/event-source")]
public async Task<HttpResponse> ServerEventsResponse(HttpRequest request)
{
var serverEvents = await request.GetEventSourceAsync ();
string[] fruits = new[] { "Apple", "Banana", "Watermelon", "Tomato" };
foreach (string fruit in fruits)
{
await serverEvents.SendAsync(fruit);
await Task.Delay(1500);
}
return await serverEvents.CloseAsync();
}
}При запуске этого кода ожидается результат, похожий на следующий:

Разрешение проксированных IP и хостов #
Sisk может использоваться с прокси, поэтому IP‑адреса могут заменяться конечной точкой прокси в транзакции от клиента к прокси.
Вы можете определить собственные резолверы в Sisk с помощью forwarding resolvers.
Кодировка заголовков #
Кодировка заголовков может быть проблемой для некоторых реализаций. В Windows заголовки UTF‑8 не поддерживаются, поэтому используется ASCII. Sisk имеет встроенный конвертер кодировок, который может быть полезен для декодирования неправильно закодированных заголовков.
Эта операция ресурсоёмка и отключена по умолчанию, но её можно включить с помощью HttpServerConfiguration.NormalizeHeadersEncodings.