# Запросы

Source: https://docs.sisk-framework.org/ru/docs/fundamentals/requests.html

Запросы — это структуры, представляющие HTTP‑сообщение запроса. Объект [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) содержит полезные функции для обработки HTTP‑сообщений во всём вашем приложении.

HTTP‑запрос состоит из метода, пути, версии, заголовков и тела.

В этом документе мы покажем, как получить каждый из этих элементов.

## Получение метода запроса

Чтобы получить метод полученного запроса, можно использовать свойство Method:

```cs
static HttpResponse Index(HttpRequest request)
{
    HttpMethod requestMethod = request.Method;
    ...
}
```

Это свойство возвращает метод запроса, представленный объектом [HttpMethod](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpmethod).

> [!NOTE]
> В отличие от методов маршрута, это свойство не обслуживает элемент [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md). Вместо этого оно возвращает реальный метод запроса.

## Получение компонентов URL запроса

Вы можете получить различные компоненты URL через определённые свойства запроса. Для примера рассмотрим URL:

```
http://localhost:5000/user/login?email=foo@bar.com
```

| Имя компонента | Описание | Значение компонента |
| --- | --- | --- |
| [Path](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Path.md) | Получает путь запроса. | `/user/login` |
| [FullPath](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullPath.md) | Получает путь запроса и строку запроса. | `/user/login?email=foo@bar.com` |
| [FullUrl](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullUrl.md) | Получает полную строку URL запроса. | `http://localhost:5000/user/login?email=foo@bar.com` |
| [Host](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Host.md) | Получает хост запроса. | `localhost` |
| [Authority](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Authority.md) | Получает хост и порт запроса. | `localhost:5000` |
| [QueryString](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.QueryString.md) | Получает строку запроса. | `?email=foo@bar.com` |
| [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) | Получает запрос в виде коллекции именованных значений. | `{StringValueCollection object}` |
| [IsSecure](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsSecure.md) | Определяет, использует ли запрос SSL (true) или нет (false). | `false` |

Вы также можете воспользоваться свойством [HttpRequest.Uri](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Uri.md), которое включает всё перечисленное в одном объекте.

## Метаданные запроса и отмена

Sisk также добавляет к каждому запросу оперативные метаданные. Эти свойства полезны для журналов, трассировки, локализации, диагностики и длительных операций:

| Свойство или метод | Назначение |
| --- | --- |
| [RequestId](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestId.md) | Уникальный идентификатор запроса. Включите [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md), чтобы возвращать его как `X-Request-Id`. |
| [RequestedAt](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestedAt.md) | Момент, когда Sisk создал объект запроса. |
| [RemoteAddress](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RemoteAddress.md) | Адрес клиента, полученный из соединения, или из вашего [ForwardingResolver](https://docs.sisk-framework.org/ru/docs/advanced/forwarding-resolvers.md). |
| [Culture](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Culture.md) | Наиболее подходящая культура, определённая из `Accept-Language`, с откатом к текущей культуре. |
| [DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) | Токен отмены, который сигнализируется при отключении клиента, если поддерживается настроенным HTTP‑движком. |
| [Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) | Типизированное хранилище ключ/значение, совместно используемое обработчиками запросов и действием маршрута. |
| [GetRawHttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRawHttpRequest.md) | Текстовое представление запроса для диагностики. |

## Получение тела запроса

Некоторые запросы содержат тело, например формы, файлы или API‑транзакции. Тело запроса можно получить через свойство:

```cs
// получает тело запроса как строку, используя кодировку запроса в качестве кодировщика
string body = request.Body;

// или получает его в виде массива байтов
byte[] bodyBytes = request.RawBody;

// либо можно получить поток.
Stream requestStream = request.GetRequestStream();

// или прочитать тело асинхронно
Memory<byte> bodyMemory = await request.GetBodyContentsAsync();
```

Также можно определить, есть ли тело в запросе и загружено ли оно, с помощью свойств [HasContents](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.HasContents.md), которое определяет наличие содержимого, и [IsContentAvailable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsContentAvailable.md), которое указывает, что HTTP‑сервер полностью получил содержимое от удалённого источника.

Невозможно прочитать содержимое запроса через `GetRequestStream` более одного раза. Если вы читаете с помощью этого метода, значения в `RawBody` и `Body` также будут недоступны. Не требуется освобождать поток запроса в контексте запроса, так как он освобождается в конце HTTP‑сессии, в которой был создан. Кроме того, вы можете использовать свойство [HttpRequest.RequestEncoding](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestEncoding.md), чтобы получить оптимальную кодировку для ручного декодирования запроса.

Сервер накладывает ограничения на чтение содержимого запроса, которые применяются как к [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md), так и к [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md). Эти свойства копируют весь входной поток в локальный буфер размером, равным [HttpRequest.ContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.ContentLength.md).

Содержимое запроса ограничивается параметром [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md), который по умолчанию составляет 100 МБ начиная с Sisk 1.7 (в более ранних версиях было `0`, без ограничений). Ограничение применяется следующим образом:

- Запрос, указывающий `Content-Length`, превышающий лимит, получает ответ `413 Content Too Large` до вызова маршрута.
- Тело, передаваемое кусками или в виде потока без объявленной длины, проверяется во время чтения. Чтение за пределы лимита через [RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md), [Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) или поток, возвращаемый [GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md), вызывает [InsufficientMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.insufficientmemoryexception), что приводит к ошибке `500 Internal Server Error`, если ваш код или [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) не обработают её.
- То же ограничение является максимальным размером получаемого сообщения [WebSocket](https://docs.sisk-framework.org/ru/docs/features/websockets.md).

Установите значение свойства в `0`, чтобы снять ограничение:

```cs
using var app = HttpServer.CreateBuilder()
    .UseConfiguration(config => config.MaximumContentLength = 0)
    .Build();
```

Даже без установленного ограничения `Body` и `RawBody` не могут буферизовать содержимое, превышающее [Array.MaxLength](https://learn.microsoft.com/en-us/dotnet/api/system.array.maxlength) (около 2 ГБ), и бросают исключение при попытке доступа к ним. По‑прежнему можно работать с большим содержимым через потоковую передачу.

> [!NOTE]
> Хотя Sisk позволяет это, всегда рекомендуется следовать семантике HTTP при создании приложения и не получать или обслуживать содержимое в методах, которые этого не допускают. Подробнее см. [RFC 9110 "HTTP Semantics"](https://httpwg.org/spec/rfc9110.html).

## Чтение JSON‑запросов

Для JSON‑API предпочтительно использовать встроенные помощники JSON вместо чтения `Body` и ручной десериализации. Они используют [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) и по умолчанию применяют [HttpRequest.DefaultJsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DefaultJsonSerializerOptions.md).

```cs
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);
});
```

Используйте асинхронную перегрузку, когда вы уже находитесь в асинхронном маршруте или хотите, чтобы отмена запроса прерывала десериализацию:

```cs
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](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions):

```cs
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
    PropertyNameCaseInsensitive = true
};

UserDto? user = request.GetJsonContent<UserDto>(options);
```

Для приложений Native AOT или чувствительных к обрезке используйте перегрузку `JsonTypeInfo<T>`, генерируемую `JsonSerializerContext`:

```cs
[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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) можно получить с помощью статического метода `HttpContext.GetCurrentContext()`. Этот метод возвращает контекст запроса, который в данный момент обрабатывается в текущем потоке.

```cs
HttpContext context = HttpContext.GetCurrentContext();
```

### Режим журналирования

Свойство [HttpContext.LogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.LogMode.md) позволяет управлять поведением журналирования для текущего запроса. Вы можете включать или отключать журналирование для отдельных запросов, переопределяя конфигурацию сервера по умолчанию.

```cs
// Отключить журналирование для этого запроса
context.LogMode = LogOutputMode.None;
```

### Корзина запросов

Объект [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) содержит сохранённую информацию, передаваемую от одного обработчика запроса к другому, и может быть использована в конечной точке. Этот объект также может использоваться обработчиками запросов, которые выполняются после обратного вызова маршрута.

> [!TIP]
> Это свойство также доступно через свойство [HttpRequest.Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md).

```cs {title="Middleware/AuthenticateUserRequestHandler.cs"}
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` в корзину запросов, и его можно будет использовать позже в финальном обратном вызове:

```cs {title="Controller/MyController.cs"}
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` для более гибкого управления.

```cs {title="Middleware/Authenticate.cs"}
public class Authenticate : RequestHandler
{
    public override HttpResponse? Execute(HttpRequest request, HttpContext context)
    {
        request.Bag.Set<User>(authUser);
    }
}
```

```csharp {title="Controller/MyController.cs"}
[RouteGet("/")]
[RequestHandler<Authenticate>]
public static HttpResponse GetUser(HttpRequest request)
{
    var user = request.Bag.Get<User>();
    ...
}
```

### Атомарные значения контекста

Корзина запросов не синхронизирована. Когда числовые значения в корзине могут изменяться конкурентным кодом в рамках одного запроса, например параллельными задачами, запущенными маршрутом, используйте [HttpContext.Interlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Interlocked.md). Он возвращает объект [HttpContextInterlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.HttpContextInterlocked.md) с атомарными операциями над именованными значениями корзины запросов:

| Метод | Описание |
| --- | --- |
| `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`.

```cs
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");
});
```

> [!NOTE]
> Операции атомарны только относительно других вызовов `Interlocked`. Чтение или запись того же ключа напрямую через `RequestBag` не синхронизированы с ними.

## Получение данных формы

Вы можете получить значения данных формы в объекте [StringKeyStoreCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringKeyStoreCollection.md) с помощью примера ниже:

```cs {title="Controller/Auth.cs"}
[RoutePost("/auth")]
public HttpResponse Index(HttpRequest request)
{
    var form = request.GetFormContent();

    string? username = form["username"];
    string? password = form["password"];

    if (AttempLogin(username, password))
    {
        ...
    }
}
```

Асинхронная версия полезна, когда тело запроса может быть большим или когда требуется поддержка отмены:

```cs
var form = await request.GetFormContentAsync(request.DisconnectToken);
```

## Получение multipart‑данных формы

HTTP‑запрос Sisk позволяет получать загруженные multipart‑содержимое, такое как файлы, поля формы или любой двоичный контент.

```cs {title="Controller/Auth.cs"}
[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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContentAsync.md), когда маршрут асинхронный:

```cs
var multipartFormDataObjects =
    await request.GetMultipartFormContentAsync(request.DisconnectToken);
```

Подробнее о [Multipart‑объектах формы](https://docs.sisk-framework.org/api/Sisk.Core.Entity.MultipartObject.md) Sisk, их методах, свойствах и возможностях, можно узнать в документации.

## Обнаружение отключения клиента

Начиная с версии v1.15 Sisk, фреймворк предоставляет токен отмены через [HttpRequest.DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md). Когда настроенный HTTP‑движок поддерживает обнаружение отключения, этот токен отменяется, если соединение клиента закрывается до завершения ответа. Это полезно для остановки длительных операций, когда клиент больше не ждёт результата.

```csharp
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](https://docs.sisk-framework.org/ru/docs/cadente.md) поддерживает `DisconnectToken`. Если ваш маршрут зависит от отмены с учётом отключения, используйте Cadente или другой движок, который явно реализует это поведение. Даже при поддерживаемом движке отмена является кооперативной: передавайте токен в асинхронные API и проверяйте его в своей длительной работе.

## Поддержка Server‑sent events

Sisk поддерживает [Server‑sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), что позволяет отправлять фрагменты как поток и поддерживать соединение между сервером и клиентом живым.

Вызов метода [HttpRequest.GetEventSource](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) переводит HttpRequest в состояние слушателя. В этом случае контекст HTTP‑запроса не будет ожидать HttpResponse, так как он будет перекрывать пакеты, отправляемые серверными событиями.

После отправки всех пакетов обратный вызов должен вернуть метод [Close](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequestEventSource.Close.md), который отправит окончательный ответ серверу и укажет, что поток завершён.

Невозможно предсказать общую длину всех отправляемых пакетов, поэтому определить конец соединения с помощью заголовка `Content-Length` невозможно.

По умолчанию большинство браузеров не поддерживают отправку HTTP‑заголовков или методов, отличных от GET, в серверных событиях. Поэтому будьте осторожны, используя обработчики запросов с event‑source, которые требуют специфических заголовков, так как, скорее всего, они не будут присутствовать.

Кроме того, большинство браузеров перезапускают потоки, если метод [EventSource.close](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close) не вызывается на клиенте после получения всех пакетов, что приводит к бесконечной дополнительной обработке на стороне сервера. Чтобы избежать такой проблемы, обычно отправляют финальный пакет, указывающий, что источник событий завершил отправку всех пакетов.

Пример ниже показывает, как браузер может взаимодействовать с сервером, поддерживающим серверные события.

```html {title="sse-example.html"}
<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>
```

И постепенно отправлять сообщения клиенту:

```cs {title="Controller/MyController.cs"}
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();
    }
}
```

При запуске этого кода ожидается результат, похожий на следующий:

<img src="/assets/img/server side events demo.gif" />

## Разрешение проксированных IP и хостов

Sisk может использоваться с прокси, поэтому IP‑адреса могут заменяться конечной точкой прокси в транзакции от клиента к прокси.

Вы можете определить собственные резолверы в Sisk с помощью [forwarding resolvers](https://docs.sisk-framework.org/ru/docs/advanced/forwarding-resolvers.md).

## Кодировка заголовков

Кодировка заголовков может быть проблемой для некоторых реализаций. В Windows заголовки UTF‑8 не поддерживаются, поэтому используется ASCII. Sisk имеет встроенный конвертер кодировок, который может быть полезен для декодирования неправильно закодированных заголовков.

Эта операция ресурсоёмка и отключена по умолчанию, но её можно включить с помощью [HttpServerConfiguration.NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md).
