Sisk

Requisições

Esta página foi traduzida automaticamente do inglês. Ler o original

Requisições são estruturas que representam uma mensagem de requisição HTTP. O objeto HttpRequest contém funções úteis para manipular mensagens HTTP em toda a sua aplicação.

Uma requisição HTTP é composta pelo método, caminho, versão, cabeçalhos e corpo.

Neste documento, ensinaremos como obter cada um desses elementos.

Obtendo o método da requisição #

Para obter o método da requisição recebida, você pode usar a propriedade Method:

C#
static HttpResponse Index(HttpRequest request)
{
    HttpMethod requestMethod = request.Method;
    ...
}

Esta propriedade devolve o método da requisição representado por um objeto HttpMethod.

Nota

Ao contrário dos métodos de rota, esta propriedade não serve ao item RouteMethod.Any. Em vez disso, ela retorna o método real da requisição.

Obtendo componentes da URL da requisição #

Você pode obter vários componentes de uma URL através de determinadas propriedades de uma requisição. Para este exemplo, considere a URL:

http://localhost:5000/user/[email protected]
Nome do componenteDescriçãoValor do componente
PathObtém o caminho da requisição./user/login
FullPathObtém o caminho da requisição e a string de consulta./user/[email protected]
FullUrlObtém a string completa da URL da requisição.http://localhost:5000/user/[email protected]
HostObtém o host da requisição.localhost
AuthorityObtém o host e a porta da requisição.localhost:5000
QueryStringObtém a string de consulta da requisição.[email protected]
QueryObtém a consulta da requisição em uma coleção de valores nomeados.{StringValueCollection object}
IsSecureDetermina se a requisição está usando SSL (true) ou não (false).false

Você também pode optar por usar a propriedade HttpRequest.Uri, que inclui tudo acima em um único objeto.

Metadados da requisição e cancelamento #

Sisk também anexa metadados operacionais a cada requisição. Essas propriedades são úteis para logs, rastreamento, localização, diagnósticos e operações de longa duração:

Propriedade ou métodoUso
RequestIdUm identificador único para a requisição. Ative IncludeRequestIdHeader para retorná-lo como X-Request-Id.
RequestedAtO momento em que o Sisk criou o objeto de requisição.
RemoteAddressO endereço do cliente resolvido a partir da conexão, ou a partir do seu ForwardingResolver.
CultureA melhor cultura resolvida a partir de Accept-Language, recuando para a cultura atual.
DisconnectTokenUm token de cancelamento sinalizado quando o cliente se desconecta, quando suportado pelo mecanismo HTTP configurado.
BagUm armazenamento tipado de chave/valor compartilhado entre manipuladores de requisição e a ação da rota.
GetRawHttpRequestUma representação textual da requisição para diagnóstico.

Obtendo o corpo da requisição #

Algumas requisições incluem corpo, como formulários, arquivos ou transações de API. Você pode obter o corpo de uma requisição a partir da propriedade:

C#
// obtém o corpo da requisição como uma string, usando a codificação da requisição como codificador
string body = request.Body;

// ou obtém como um array de bytes
byte[] bodyBytes = request.RawBody;

// ou então, você pode transmiti-lo como stream.
Stream requestStream = request.GetRequestStream();

// ou lê o corpo de forma assíncrona
Memory<byte> bodyMemory = await request.GetBodyContentsAsync();

Também é possível determinar se há um corpo na requisição e se ele está carregado com as propriedades HasContents, que determina se a requisição tem conteúdo, e IsContentAvailable que indica que o servidor HTTP recebeu completamente o conteúdo do ponto remoto.

Não é possível ler o conteúdo da requisição através de GetRequestStream mais de uma vez. Se você ler com este método, os valores em RawBody e Body também não ficarão disponíveis. Não é necessário descartar o stream da requisição no contexto da requisição, pois ele é descartado ao final da sessão HTTP em que foi criado. Além disso, você pode usar a propriedade HttpRequest.RequestEncoding para obter a melhor codificação e decodificar a requisição manualmente.

O servidor tem limites para a leitura do conteúdo da requisição, que se aplicam tanto a HttpRequest.Body quanto a HttpRequest.RawBody. Essas propriedades copiam todo o stream de entrada para um buffer local do mesmo tamanho de HttpRequest.ContentLength.

O conteúdo da requisição é limitado por HttpServerConfiguration.MaximumContentLength, que por padrão é 100 MB desde o Sisk 1.7 (era 0, ilimitado, em versões anteriores). O limite se aplica da seguinte forma:

  • Uma requisição que declara um Content-Length maior que o limite recebe uma resposta 413 Content Too Large antes que a rota seja invocada.
  • Um corpo chunked ou transmitido sem comprimento declarado é verificado enquanto é lido. Ler além do limite através de RawBody, Body ou do stream retornado por GetRequestStream lança uma InsufficientMemoryException, que resulta em um 500 Internal Server Error a menos que seu código ou Router.CallbackErrorHandler o trate.
  • O mesmo limite é o tamanho máximo de uma mensagem WebSocket recebida.

Defina a propriedade como 0 para remover o limite:

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

Mesmo sem um limite configurado, Body e RawBody não podem armazenar em buffer conteúdo maior que Array.MaxLength (cerca de 2 GB) e lançam uma exceção quando você tenta acessá‑los. Ainda assim, é possível lidar com conteúdo maior por meio de streaming.

Nota

Embora o Sisk permita, é sempre uma boa prática seguir a Semântica HTTP ao criar sua aplicação e não obter ou servir conteúdo em métodos que não o permitem. Leia sobre RFC 9110 “HTTP Semantics”.

Lendo requisições JSON #

Para APIs JSON, prefira os auxiliares JSON embutidos em vez de ler Body e desserializar manualmente. Eles utilizam System.Text.Json e, por padrão, HttpRequest.DefaultJsonSerializerOptions.

C#
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);
});

Use a sobrecarga assíncrona quando já estiver em uma rota async ou quiser que o cancelamento da requisição interrompa a desserialização:

C#
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);
});

Você pode passar opções personalizadas de JsonSerializerOptions para um endpoint específico:

C#
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
    PropertyNameCaseInsensitive = true
};

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

Para aplicações Native AOT ou sensíveis a trimming, use a sobrecarga JsonTypeInfo<T> gerada por um JsonSerializerContext:

C#
[JsonSerializable(typeof(CreateUserRequest))]
public partial class AppJsonSerializerContext : JsonSerializerContext
{
}

CreateUserRequest? body =
    await request.GetJsonContentAsync(
        AppJsonSerializerContext.Default.CreateUserRequest,
        request.DisconnectToken);

A mesma regra de leitura única se aplica aos auxiliares JSON: depois que o Sisk lê o stream da requisição através de GetJsonContent, GetJsonContentAsync, Body ou RawBody, não é possível consumir novamente o mesmo corpo via GetRequestStream().

Obtendo o contexto da requisição #

O HTTP Context é um objeto exclusivo do Sisk que armazena informações do servidor HTTP, rota, roteador e manipulador de requisição. Você pode usá‑lo para organizar-se em um ambiente onde esses objetos são difíceis de organizar.

Você pode obter o HttpContext em execução atualmente usando o método estático HttpContext.GetCurrentContext(). Esse método devolve o contexto da requisição que está sendo processada no thread atual.

C#
HttpContext context = HttpContext.GetCurrentContext();

Modo de Log #

A propriedade HttpContext.LogMode permite controlar o comportamento de logging para a requisição atual. Você pode habilitar ou desabilitar o logging para requisições específicas, sobrescrevendo a configuração padrão do servidor.

C#
// Desabilitar o logging para esta requisição
context.LogMode = LogOutputMode.None;

Request Bag #

O objeto RequestBag contém informações armazenadas que são passadas de um manipulador de requisição para outro ponto, podendo ser consumidas no destino final. Esse objeto também pode ser usado por manipuladores que são executados após o callback da rota.

Dica

Esta propriedade também está acessível pela propriedade HttpRequest.Bag.

Middleware/AuthenticateUserRequestHandler.csC#
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);
        }
    }
}

O manipulador acima definirá AuthenticatedUser no request bag, podendo ser consumido posteriormente no callback final:

Controller/MyController.csC#
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}!")
        };
    }
}

Você também pode usar os métodos auxiliares Bag.Set() e Bag.Get() para obter ou definir objetos pelo seu tipo singleton.

A classe TypedValueDictionary também fornece os métodos GetValue e SetValue para maior controle.

Middleware/Authenticate.csC#
public class Authenticate : RequestHandler
{
    public override HttpResponse? Execute(HttpRequest request, HttpContext context)
    {
        request.Bag.Set<User>(authUser);
    }
}
Controller/MyController.csC#
[RouteGet("/")]
[RequestHandler<Authenticate>]
public static HttpResponse GetUser(HttpRequest request)
{
    var user = request.Bag.Get<User>();
    ...
}

Valores atômicos de contexto #

O request bag não é sincronizado. Quando valores numéricos no bag podem ser alterados por código concorrente durante a mesma requisição, como tarefas paralelas iniciadas pela rota, use HttpContext.Interlocked. Ele devolve um objeto HttpContextInterlocked com operações atômicas sobre valores nomeados do request bag:

MétodoDescrição
Add(name, value)Adiciona value ao valor armazenado, ou armazena value quando name não está definido.
Exchange(name, value)Substitui o valor armazenado e devolve o anterior, ou value quando name não estava definido.
CompareExchange(name, value, comparand, notFound = -1)Substitui o valor armazenado por value somente quando ele é igual a comparand, e devolve o valor original, ou notFound quando name não está definido.
Inspect(name)Retorna o valor armazenado, ou null quando name não está definido.

Os valores são armazenados como double no request bag. As sobrecargas que recebem int ou long convertem esses valores para double.

C#
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");
});

Nota

As operações são atômicas apenas em relação a outras chamadas Interlocked. Ler ou escrever a mesma chave diretamente através de RequestBag não é sincronizado com elas.

Obtendo dados de formulário #

Você pode obter valores de dados de formulário em uma StringKeyStoreCollection com o exemplo abaixo:

Controller/Auth.csC#
[RoutePost("/auth")]
public HttpResponse Index(HttpRequest request)
{
    var form = request.GetFormContent();

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

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

A versão assíncrona é útil quando o corpo da requisição pode ser grande ou quando você deseja suporte a cancelamento:

C#
var form = await request.GetFormContentAsync(request.DisconnectToken);

Obtendo dados de formulário multipart #

O request HTTP do Sisk permite obter conteúdos multipart enviados, como arquivos, campos de formulário ou qualquer conteúdo binário.

Controller/Auth.csC#
[RoutePost("/upload-contents")]
public HttpResponse Index(HttpRequest request)
{
    // o método a seguir lê toda a entrada da requisição em um
    // array de MultipartObjects
    var multipartFormDataObjects = request.GetMultipartFormContent();
    
    foreach (MultipartObject uploadedObject in multipartFormDataObjects)
    {
        // O nome do arquivo fornecido pelos dados de formulário multipart.
        // Null é retornado se o objeto não for um arquivo.
        Console.WriteLine("File name       : " + uploadedObject.Filename);

        // O nome do campo do objeto de dados de formulário multipart.
        Console.WriteLine("Field name      : " + uploadedObject.Name);

        // O comprimento do conteúdo do objeto de dados de formulário multipart.
        Console.WriteLine("Content length  : " + uploadedObject.ContentLength);

        // Determina o formato da imagem com base no cabeçalho do arquivo para cada
        // tipo de conteúdo conhecido. Se o conteúdo não for um arquivo comum reconhecido,
        // este método abaixo retornará MultipartObjectCommonFormat.Unknown
        Console.WriteLine("Common format   : " + uploadedObject.GetCommonFileFormat());
    }
}

Use GetMultipartFormContentAsync quando a rota for assíncrona:

C#
var multipartFormDataObjects =
    await request.GetMultipartFormContentAsync(request.DisconnectToken);

Você pode ler mais sobre os objetos de formulário multipart do Sisk e seus métodos, propriedades e funcionalidades.

Detectando desconexão do cliente #

Desde a versão v1.15 do Sisk, o framework fornece um token de cancelamento através de HttpRequest.DisconnectToken. Quando o mecanismo HTTP configurado suporta detecção de desconexão, esse token é cancelado quando a conexão do cliente é fechada antes que a resposta seja concluída. Isso é útil para interromper operações de longa duração quando o cliente não está mais aguardando o resultado.

C#
router.MapGet("/connect", async (HttpRequest req) =>
{
    // obtém o token de desconexão da requisição
    var dc = req.DisconnectToken;

    await LongOperationAsync(dc);

    return new HttpResponse();
});

Esse token não é compatível com todos os mecanismos HTTP, e cada um requer uma implementação.

O mecanismo padrão do Sisk, baseado em System.Net.HttpListener, não suporta detecção de desconexão do cliente. Quando sua aplicação usa o mecanismo padrão, DisconnectToken é CancellationToken.None; na prática, trata‑se de um token que não pode ser cancelado e deve ser considerado indisponível.

O motor Cadente suporta DisconnectToken. Se sua rota depende de cancelamento consciente de desconexão, use Cadente ou outro motor que implemente explicitamente esse comportamento. Mesmo com um motor suportado, o cancelamento é cooperativo: passe o token para APIs assíncronas e verifique‑o em seu próprio trabalho de longa duração.

Suporte a eventos enviados pelo servidor #

Sisk suporta Server-sent events, que permite enviar blocos como um stream e manter a conexão entre o servidor e o cliente viva.

Chamar o método HttpRequest.GetEventSource colocará o HttpRequest em estado de escuta. A partir disso, o contexto desta requisição HTTP não esperará um HttpResponse, pois ele sobreporá os pacotes enviados pelos eventos do lado do servidor.

Após enviar todos os pacotes, o callback deve retornar o método Close, que enviará a resposta final ao cliente e indicará que o streaming terminou.

Não é possível prever o comprimento total de todos os pacotes que serão enviados, portanto não é possível determinar o fim da conexão com o cabeçalho Content-Length.

Pela maioria dos navegadores, eventos do lado do servidor não suportam o envio de cabeçalhos HTTP ou métodos diferentes de GET. Portanto, tenha cuidado ao usar manipuladores de requisição com solicitações de event‑source que exigem cabeçalhos específicos, pois provavelmente eles não estarão presentes.

Além disso, a maioria dos navegadores reinicia streams se o método EventSource.close não for chamado no cliente após receber todos os pacotes, causando processamento infinito adicional no servidor. Para evitar esse tipo de problema, é comum enviar um pacote final indicando que o event source terminou de enviar todos os pacotes.

O exemplo abaixo mostra como o navegador pode se comunicar com o servidor que suporta Server‑side events.

sse-example.htmlHTML
<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>

E envie progressivamente as mensagens ao cliente:

Controller/MyController.csC#
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();
    }
}

Ao executar este código, esperamos um resultado semelhante a este:

Resolvendo IPs e hosts proxyados #

Sisk pode ser usado com proxies, e portanto endereços IP podem ser substituídos pelo endpoint do proxy na transação de um cliente para o proxy.

Você pode definir seus próprios resolvers no Sisk com forwarding resolvers.

Codificação de cabeçalhos #

A codificação de cabeçalhos pode ser um problema para algumas implementações. No Windows, cabeçalhos UTF‑8 não são suportados, portanto ASCII é usado. O Sisk possui um conversor de codificação embutido, que pode ser útil para decodificar cabeçalhos codificados incorretamente.

Essa operação é custosa e está desabilitada por padrão, mas pode ser habilitada com HttpServerConfiguration.NormalizeHeadersEncodings.

Digite para pesquisar na documentação e na referência da API.