# 路由

Source: https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.html

The [Router](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.md) 是构建服务器的第一步。它负责容纳 [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) 对象，这些对象是将 URL 及其方法映射到服务器执行的操作的端点。每个操作负责接收请求并向客户端返回响应。

路由是路径表达式（“路径模式”）与其可监听的 HTTP 方法的配对。当向服务器发出请求时，服务器会尝试找到匹配该请求的路由，然后调用该路由的操作并将产生的响应返回给客户端。

在 Sisk 中定义路由有多种方式：可以是静态的、动态的或自动扫描的，亦可通过属性定义，或直接在 Router 对象中定义。

```cs
Router mainRouter = new Router();

// maps the GET / route into the following action
mainRouter.MapGet("/", request => {
    return new HttpResponse("Hello, world!");
});
```

要了解路由能够做什么，需要先了解请求能够做什么。一个 [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) 将包含你所需的一切。Sisk 还提供了一些额外特性，以加快整体开发速度。

对于服务器接收到的每个操作，都会调用类型为 [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md) 的委托。该委托包含一个参数，持有一个 [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md)，其中包含关于服务器收到的请求的所有必要信息。该委托返回的对象必须是 [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) 或通过 [implicit response types](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/responses.md#implicit-response-types) 映射到它的对象。

## Matching routes

当 HTTP 服务器收到请求时，Sisk 会搜索满足请求路径表达式的路由。该表达式始终在路由和请求路径之间进行测试，不考虑查询字符串。

第一个路径和方法匹配请求的路由会处理该请求。当没有路由匹配该请求时，返回 [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) 的响应给客户端。当路径模式匹配但 HTTP 方法不匹配时，返回 [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) 的响应给客户端。路由的测试顺序在 [matching precedence](#matching-precedence) 中描述。

Sisk 会检查静态路由之间是否可能出现冲突。当服务器启动时，Sisk 会查找路径模式和方法重叠的静态路由，若发现则抛出异常。在同一次检查中，若有两个或更多路由共享相同的 [Route.Name](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Name.md)，Sisk 也会抛出 `ArgumentException`。这两项检查受 [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md) 控制，默认启用。

### Matching precedence

每个路由要么是 **静态** 要么是 **动态**，由 [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) 指示：

- **Static routes** 严格遵循其路径模式，例如 `/users/<id>`。使用 `Map*` 方法、`Route` 实例和路由属性创建的路由都是静态的。
- **Dynamic routes** 以自己的方式解释路径，可能匹配无法用路径模式描述的路径。[RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md)、[PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md)（用于 [file servers](https://docs.sisk-framework.org/zh-cn/docs/features/file-server.md)）以及 [custom matching routes](#custom-route-matching) 属于动态路由。

当服务器启动时，路由器会重新排序其路由，使所有静态路由在所有动态路由之前进行测试。每个组内部保持注册顺序。这意味着静态路由始终优先于动态路由，无论它们的定义顺序如何：

```cs
// dynamic: serves any GET path from the "wwwroot" directory
mainRouter.MapFileSystem("/", "wwwroot");

// static: handles GET /api/status, even though it was defined after the file server
mainRouter.MapGet("/api/status", request => new HttpResponse("ok"));
```

在上面的示例中，文件服务器充当了所有未被静态路由处理的 GET 请求的回退。

两个组在验证和转换方式上也有所不同：

| | 静态路由 | 动态路由 |
|---|---|---|
| 匹配顺序 | 首先，按注册顺序 | 在所有静态路由之后，按注册顺序 |
| 路由冲突检查 | 是 | 否 |
| [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) 是否应用 | 是 | 否 |

> [!WARNING]
> 动态路由不会进行冲突检查，因为 Sisk 无法知道它们匹配哪些路径。当两个动态路由可以匹配同一请求时，先定义的路由获胜。例如，先映射到 `/` 的文件服务器会使随后映射到 `/static` 的文件服务器不可达。

> [!IMPORTANT]
> 在 1.7 之前的版本中，路由仅按注册顺序测试，正则路由可能会优先于之后定义的静态路由。如果你的应用依赖正则路由处理同样被静态路由匹配的路径，请将该逻辑移至静态路由或缩小静态路径模式的范围。

### Creating routes using path patterns

对于新应用，推荐使用 `Map*` 方法。它们在调用位置即可看到 HTTP 方法，并且符合当前的 `Router` API。旧的 `SetRoute` 方法仍作为兼容包装存在，但新示例应使用 `Map`、`MapGet`、`MapPost`、`MapPut`、`MapDelete`、`MapPatch`、`MapAny`、`MapOptions` 或 `MapHead`。

```cs
// Map* methods are the usual way to define method-specific routes.
mainRouter.MapGet("/hey/<name>", (request) =>
{
    string name = request.RouteParameters["name"].GetString();
    return new HttpResponse($"Hello, {name}");
});

mainRouter.MapPost("/form", (request) =>
{
    var formData = request.GetFormContent();
    return new HttpResponse(); // empty 200 ok
});

// Map can also receive a Route instance when you need route options.
mainRouter.Map(Route.Get("/image.png", (request) =>
{
    var imageStream = File.OpenRead("image.png");
    
    return new HttpResponse()
    {
        // the StreamContent inner
        // stream is disposed after sending
        // the response.
        Content = new StreamContent(imageStream)
    };
}));

// multiple parameters
mainRouter.MapGet("/hey/<name>/surname/<surname>", (request) =>
{
    string name = request.RouteParameters["name"].GetString();
    string surname = request.RouteParameters["surname"].GetString();

    return new HttpResponse($"Hello, {name} {surname}!");
});
```

[RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) 属性包含了收到请求的路径变量的所有信息。

服务器接收到的每个路径在执行路径模式测试之前都会被规范化，规则如下：

- 所有空段都会从路径中移除，例如：`////foo//bar` 会变为 `/foo/bar`。
- 路径匹配 **区分大小写**，除非将 [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) 设置为 `true`。

[Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) 和 [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) 属性返回一个 [StringValueCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValueCollection.md) 对象，其中每个索引属性返回一个非空的 [StringValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValue.md)，可用作选项/单子，将其原始值转换为受管理的对象。

下面的示例读取路由参数 “id”，并从中获取一个 `Guid`。如果参数不是有效的 Guid，会抛出异常；如果服务器未处理 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md)，则返回 500 错误给客户端。

```cs
mainRouter.MapGet("/user/<id>", (request) =>
{
    Guid id = request.RouteParameters["id"].GetGuid();
    return new HttpResponse($"User id: {id}");
});
```

> [!NOTE]
> 路径的尾部 `/` 在请求路径和路由路径中都会被忽略，也就是说，如果你尝试访问定义为 `/index/page` 的路由，同样可以使用 `/index/page/` 进行访问。
>
> 你也可以通过启用 [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) 强制 URL 以 `/` 结尾。

### Creating routes using class instances

你也可以使用属性 [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md) 通过反射动态定义路由。这样，类的实例中实现了该属性的方法将在目标路由器中定义路由。

要将方法定义为路由，必须使用 [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md) 标记，例如该属性本身或 [RouteGetAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteGetAttribute.md)。方法可以是 static、实例、public 或 private。希望从对象映射实例和静态路由方法时使用 `MapInstance`。希望仅从类型映射静态路由方法时使用 `MapType`。

```cs {title="Controller/MyController.cs"}
public class MyController
{
    // will match GET /
    [RouteGet]
    HttpResponse Index(HttpRequest request)
    {
        HttpResponse res = new HttpResponse();
        res.Content = new StringContent("Index!");
        return res;
    }
    
    // static methods works too
    [RouteGet("/hello")]
    static HttpResponse Hello(HttpRequest request)
    {
        HttpResponse res = new HttpResponse();
        res.Content = new StringContent("Hello world!");
        return res;
    }
}
```

下面的代码会将 `MyController` 的 `Index` 与 `Hello` 方法都定义为路由，因为两者都被标记为路由，并且提供了类的实例，而不是类型。如果提供的是类型，则仅会定义静态方法。

```cs
var myController = new MyController();
mainRouter.MapInstance(myController);
```

若只想映射类型中的静态路由方法，使用：

```cs
mainRouter.MapType<MyController>();
```

自 Sisk 0.16 版本起，可启用 AutoScan，自动搜索实现了 `RouterModule` 的用户自定义类并将其自动关联到路由器。AOT 编译不支持此功能。

```cs
mainRouter.AutoScanModules<ApiController>();
```

上述指令会搜索所有实现了 `ApiController` 的类型，但 **不包括该类型本身**。两个可选参数指示方法搜索这些类型的方式。第一个参数暗示搜索的程序集，第二个参数指示类型的定义方式。

## Regex routes

如果不想使用默认的 HTTP 路径匹配方式，可以使用 [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) 类将路径解释为正则表达式来定义路由。正则路由属于 [dynamic routes](#matching-precedence)。

```cs
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request =>
{
    return new HttpResponse("hello, world");
}));
```

你还可以将正则模式中的捕获组写入 [HttpRequest.RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md)：

```cs {title="Controller/MyController.cs"}
public class MyController
{
    [RegexRoute(RouteMethod.Get, @"/uploads/(?<filename>.*\.(jpeg|jpg|png))")]
    static HttpResponse RegexRoute(HttpRequest request)
    {
        string filename = request.RouteParameters["filename"].GetString();
        return new HttpResponse().WithContent($"Acessing file {filename}");
    }
}
```

> [!NOTE]
> 正则表达式不会自动锚定：例如 `/uploads` 也会匹配 `/files/uploads/a`。使用 `^` 和 `$` 可匹配完整路径。

> [!IMPORTANT]
> 自 Sisk 1.7 起，`Route.UseRegex` 属性不再可设置。该属性已废弃，仅用于报告路由是否为 [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md)。路由属性中的 `UseRegex`（如 `[Route(RouteMethod.Get, "...", UseRegex = true)]`）以及 `[RegexRoute]` 属性仍然有效。

要迁移使用 `Route.UseRegex` 创建的正则路由，请改为使用 `RegexRoute`：

```cs
// before 1.7
mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?<id>\d+)$", action) { UseRegex = true });

// 1.7 and later
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?<id>\d+)$", action));
```

## Prefix routes

[PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) 匹配其路径以及其下的所有嵌套路径，比较完整的路径段。它是 [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md) 使用的路由。前缀路由属于 [dynamic routes](#matching-precedence)。

```cs
// matches /docs, /docs/ and /docs/any/nested/path, but not /docsfoo
mainRouter.Map(new PrefixRoute(RouteMethod.Get, "/docs", null, request =>
{
    return new HttpResponse($"Requested {request.Path}");
}, null));
```

## Custom route matching

通过继承 [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) 并重写 [Route.Match](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Match.md) 可以创建自定义匹配规则。该方法接收请求路径和正在匹配该路由的路由器，并返回一个包含结果以及可选捕获路由参数的 [RouteMatch](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.md)。

路由器仅在 `Match` 成功后才会验证 HTTP 方法，因此 `Match` 应仅测试路径。这保证了 [405 Method Not Allowed](#method-not-allowed-405-callback-handler) 和 `OPTIONS` 响应在自定义路由中仍然有效。

```cs {title="Routing/ExtensionRoute.cs"}
// matches any path ending with the given file extension, such as /reports/2024.csv
public sealed class ExtensionRoute : Route
{
    public ExtensionRoute(string extension, RouteAction action)
        : base(RouteMethod.Get, extension, action)
    {
    }

    // this route does not follow a path pattern
    public override bool IsStatic => false;

    // the path is not a path pattern, so it cannot be redirected to its trailing-slash form
    public override bool AllowRewrites => false;

    public override RouteMatch Match(string requestPath, Router router)
    {
        StringComparison comparison = router.MatchRoutesIgnoreCase
            ? StringComparison.OrdinalIgnoreCase
            : StringComparison.Ordinal;

        if (!requestPath.EndsWith(Path, comparison))
            return RouteMatch.NotMatched;

        var parameters = new NameValueCollection
        {
            ["file"] = requestPath[..^Path.Length]
        };
        return new RouteMatch(true, parameters);
    }
}
```

```cs
mainRouter.Map(new ExtensionRoute(".csv", request =>
{
    string file = request.RouteParameters["file"].GetString();
    return new HttpResponse($"Exporting {file} as CSV");
}));
```

在重写 `Match` 时，请注意：

- 当路由不严格遵循路径模式时，覆盖 [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) 并返回 `false`。否则，路由器在检查冲突和应用 [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) 时会将其 `Path` 当作路径模式，可能导致错误的冲突或无效路径。
- 当路由路径无法通过 [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) 重定向到其尾部斜杠形式时，覆盖 [Route.AllowRewrites](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AllowRewrites.md) 并返回 `false`。
- `Match` 会在每个到达该路由的请求上被调用，可能会被多个线程并发调用。路由不匹配时应避免分配，并确保任何缓存状态是线程安全的。对不成功的匹配返回 [RouteMatch.NotMatched](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.NotMatched.md)。
- 通过 `router` 参数遵循 [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md)。

## Prefixing routes

可以使用 [RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) 属性为类或模块中的所有路由添加前缀，并将前缀设为字符串。

下面的示例展示了 BREAD 架构（Browse, Read, Edit, Add and Delete）：

```cs {title="Controller/Api/UsersController.cs"}
[RoutePrefix("/api/users")]
public class UsersController
{
    // GET /api/users
    [RouteGet]
    public async Task<HttpResponse> Browse()
    {
        ...
    }
    
    // GET /api/users/<id>
    [RouteGet("/<id>")]
    public async Task<HttpResponse> Read()
    {
        ...
    }
    
    // PATCH /api/users/<id>
    [RoutePatch("/<id>")]
    public async Task<HttpResponse> Edit()
    {
        ...
    }
    
    // POST /api/users
    [RoutePost]
    public async Task<HttpResponse> Add()
    {
        ...
    }
    
    // DELETE /api/users/<id>
    [RouteDelete("/<id>")]
    public async Task<HttpResponse> Delete()
    {
        ...
    }
}
```

在上述示例中，省略了 HttpResponse 参数，转而通过全局上下文 [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md) 使用。后续章节将进一步说明。

## Routes without request parameter

路由可以在没有 [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) 参数的情况下定义，并仍然能够在请求上下文中获取请求及其组件。下面考虑一个抽象类 `ControllerBase`，它作为 API 所有控制器的基础，并提供 `Request` 属性以获取当前的 [HttpRequest]。

```cs {title="Controller/ControllerBase.cs"}
public abstract class ControllerBase
{
    // gets the request from the current thread
    public HttpRequest Request { get => HttpContext.Current.Request; }
    
    // the line below, when called, gets the database from the current HTTP session,
    // or creates a new one if it doesn't exist
    public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd<DbContext>(); }
}
```

所有子类均可在不使用请求参数的情况下使用路由语法：

```cs {title="Controller/UsersController.cs"}
[RoutePrefix("/api/users")]
public class UsersController : ControllerBase
{    
    [RoutePost]
    public async Task<HttpResponse> Create()
    {
        // reads the JSON data from the current request
        UserCreationDto? user = await Request.GetJsonContentAsync<UserCreationDto>();
        ...
        Database.Users.Add(user);
        
        return new HttpResponse(201);
    }
}
```

更多关于当前上下文和依赖注入的细节，请参阅 [dependency injection](https://docs.sisk-framework.org/zh-cn/docs/features/instancing.md) 教程。

## Any method routes

你可以定义仅通过路径匹配而忽略 HTTP 方法的路由。这在需要在路由回调内部进行方法验证时非常有用。

```cs
// will match / on any HTTP method
mainRouter.MapAny("/", callbackFunction);
```

[RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 包含 `GET`、`POST`、`PUT`、`PATCH`、`DELETE`、`QUERY`、`HEAD` 和 `OPTIONS`。

## QUERY method routes

Sisk 支持 HTTP `QUERY` 方法，这是一种安全且幂等的方式，将查询放在请求体中。使用 [Router.MapQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapQuery.md)、[Route.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Query.md)、[RouteMethod.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 或 [RouteQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteQueryAttribute.md) 属性来定义这些路由：

```cs
mainRouter.MapQuery("/search", request =>
{
    string filter = request.Body;
    return new HttpResponse($"Searching for {filter}");
});
```

```cs {title="Controller/SearchController.cs"}
public class SearchController
{
    [RouteQuery("/products")]
    static HttpResponse SearchProducts(HttpRequest request)
    {
        return new HttpResponse($"Searching for {request.Body}");
    }
}
```

`Accept-Query` 头部用于声明在 `QUERY` 请求中接受的媒体类型，可通过 [HttpHeaderCollection.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.AcceptQuery.md) 和 [HttpKnownHeaderNames.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpKnownHeaderNames.AcceptQuery.md) 访问。

## Any path routes

任意路径路由会测试 HTTP 服务器收到的任何路径，前提是路由方法也被测试。如果路由方法是 `RouteMethod.Any` 且路由在路径表达式中使用了 [Route.AnyPath](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md)，则该路由会监听所有来自 HTTP 服务器的请求，且不能再定义其他路由。

```cs
// the following route will match all POST requests
mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction);
```

## Ignore case route matching

默认情况下，路由与请求的匹配是区分大小写的。若要忽略大小写，请启用此选项：

```cs
mainRouter.MatchRoutesIgnoreCase = true;
```

这同样会为正则路由启用 `RegexOptions.IgnoreCase`，并为前缀路由启用不区分大小写的比较。

## Not Found (404) callback handler

你可以为未匹配到任何已知路由的请求创建自定义回调。

```cs
mainRouter.NotFoundErrorHandler = () =>
{
    return new HttpResponse(404)
    {
        // Since v0.14
        Content = new HtmlContent("<h1>Not found</h1>")
        // older versions
        Content = new StringContent("<h1>Not found</h1>", Encoding.UTF8, "text/html")
    };
};
```

## Method not allowed (405) callback handler

你也可以为路径匹配但方法不匹配的请求创建自定义回调。

```cs
mainRouter.MethodNotAllowedErrorHandler = (context) =>
{
    return new HttpResponse(405)
    {
        Content = new StringContent($"Method not allowed for this route.")
    };
};
```

## Error Handling

在请求生命周期内（从预执行请求处理程序、通过路由操作、到后执行请求处理程序和数值处理程序）可能会抛出异常。这些异常由以下机制管理：

- 如果 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 为 `true`，异常会正常抛出，Sisk 不会捕获，且如果未捕获异常，HTTP 服务器可能会中断。
- 如果 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 为 `false`，异常会被 Sisk 捕获并处理。随后，如果已定义 `Router.CallbackErrorHandler`，它会使用捕获的异常和请求上下文被调用，并且 **不会** 转发到标准错误输出。如果未定义 `Router.CallbackErrorHandler`，异常会转发到标准错误输出，客户端将收到 HTTP 500 错误响应。如果未定义标准错误输出，错误将被静默忽略。

注意：在 `Router.CallbackErrorHandler` 中，你可以设置错误日志、访问日志、两者或都不记录的模式，并修改默认的日志写入行为：

```csharp
router.CallbackErrorHandler = (ex, ctx) =>
{
    ctx.LogMode = LogOutput.Both; // override log mode to log the error in both access and error logs
}
```

## Internal error handler

路由回调在服务器执行期间可能抛出错误。如果未正确处理，HTTP 服务器的整体功能可能会被终止。路由器提供了一个回调，用于在路由回调失败时防止服务中断。

此方法仅在 [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 设置为 false 时可达。

```cs
mainRouter.CallbackErrorHandler = (ex, context) =>
{
    return new HttpResponse(500)
    {
        Content = new StringContent($"Error: {ex.Message}")
    };
};
```
