# 请求

Source: https://docs.sisk-framework.org/zh-cn/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/zh-cn/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) 限制，自 Sisk 1.7 起默认 100 MB（早期版本为 `0`，即无限制）。限制的适用方式如下：

- 如果请求声明的 `Content-Length` 超过限制，服务器将在路由调用前返回 `413 Content Too Large` 响应。
- 对于未声明长度的分块或流式正文，在读取时会进行检查。通过 [RawBody]、[Body] 或 [GetRequestStream] 返回的流读取超出限制时会抛出 [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/zh-cn/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 GB）的内容，访问时会抛出异常。仍可通过流式处理更大的内容。

> [!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 或对裁剪敏感的应用程序，使用由 `JsonSerializerContext` 生成的 `JsonTypeInfo<T>` 重载：

```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.GetCurrentContext()` 获取当前正在执行的 [HttpContext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md)。该方法返回当前线程中正在处理的请求的上下文。

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

### 日志模式

[HttpContext.LogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.LogMode.md) 属性允许你控制当前请求的日志行为。可以为特定请求启用或禁用日志，覆盖默认的服务器配置。

```cs
// Disable logging for this request
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` 加到已存储的值上；如果 `name` 未设置，则存储 `value`。 |
| `Exchange(name, value)` | 替换已存储的值并返回之前的值；如果 `name` 未设置，则返回 `value`。 |
| `CompareExchange(name, value, comparand, notFound = -1)` | 仅当已存储的值等于 `comparand` 时才用 `value` 替换，并返回原始值；如果 `name` 未设置，则返回 `notFound`。 |
| `Inspect(name)` | 返回已存储的值；如果 `name` 未设置，则返回 `null`。 |

值在请求包中以 `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 表单数据

Sisk 的 HTTP 请求允许获取上传的 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);
```

您可以进一步了解 Sisk 的 [Multipart 表单对象](https://docs.sisk-framework.org/api/Sisk.Core.Entity.MultipartObject.md) 及其方法、属性和功能。

## 检测客户端断开连接

自 Sisk v1.15 起，框架通过 [HttpRequest.DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) 提供取消令牌。当配置的 HTTP 引擎支持断开检测时，如果客户端在响应完成前关闭连接，该令牌会被取消。这对于在客户端不再等待结果时停止长时间运行的操作非常有用。

```csharp
router.MapGet("/connect", async (HttpRequest req) =>
{
    // 从请求获取断开连接令牌
    var dc = req.DisconnectToken;

    await LongOperationAsync(dc);

    return new HttpResponse();
});
```

此令牌并非所有 HTTP 引擎都兼容，每个引擎都需要相应实现。

默认的 Sisk 引擎基于 `System.Net.HttpListener`，不支持客户端断开检测。当使用默认引擎时，`DisconnectToken` 为 `CancellationToken.None`；实际上它是一个不产生取消的令牌，应视为不可用。

[Cadente 引擎](https://docs.sisk-framework.org/zh-cn/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` 头部来确定连接的结束。

大多数浏览器默认情况下，服务器端事件不支持发送除 GET 方法之外的 HTTP 头或方法。因此，在使用需要特定请求头的 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 地址可能会被代理端点替代。

您可以使用 [forwarding resolvers](https://docs.sisk-framework.org/zh-cn/docs/advanced/forwarding-resolvers.md) 在 Sisk 中定义自己的解析器。

## 头部编码

头部编码在某些实现中可能会出现问题。在 Windows 上不支持 UTF-8 头部，因此使用 ASCII。Sisk 内置了编码转换器，可用于解码错误编码的头部。

此操作成本较高，默认已禁用，但可以通过 [HttpServerConfiguration.NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) 启用。
