GPT

这是个非常典型、而且只有做到你这个阶段才会遇到的“框架级痛点” 👍
你问的本质不是 “能不能改 method / path”,而是:

Request 在你的架构里到底是「不可变的事实」还是「可演化的视图」?

下面我会给你 4 种可行方案,从「最保守」到「最框架化」,并且明确推荐我认为最优雅、最可持续的一种,同时解释 为什么

一、先说结论(TL;DR)

我最推荐的方案:👉「Request 是不可变原始事实 + Context 中维护一个可变的 RequestView」

也就是:

这是 Koa / Spring WebFlux / Envoy / Nginx 等成熟系统背后的共识模型。

下面展开。

二、方案一:直接让 Request 可变(❌ 不推荐)

做法

class Request(
    var method: String,
    var path: String,
    ...
)

middleware 里直接:

ctx.request.method = "PUT"
ctx.request.path = "/users/1"

优点

致命问题(为什么我不推荐)

  1. 破坏语义稳定性

    • Router 匹配前后看到的是同一个对象

    • debug 时你不知道“原始请求”是什么

  2. Undertow adapter 和 core 强耦合

    • server adapter 失去边界

    • 以后换 Netty / Jetty 会更痛

  3. 子 App / 中间件顺序会变得危险

    middleware A 改 path
    middleware B 依赖原 path

    没有任何保护

📌 结论
这是“应用层 hack”,不是“框架层设计”。

三、方案二:返回一个新的 Request(函数式风格,⚠️ 半可行)

做法

middleware 改为:

fun interface Middleware {
    suspend fun invoke(ctx: Context, next: suspend (Context) -> Unit)
}

并支持:

ctx.withRequest(newRequest) {
    next()
}

或:

ctx.request = ctx.request.copy(method = "PUT")

优点

问题

  1. Context 变复杂

    • copy / restore

    • 嵌套中间件会出 bug

  2. 性能与心智负担

    • 高频 copy

    • 用户要理解作用域

  3. 和你当前设计冲突

    • 你不是纯函数式 runtime

    • middleware 是 imperative 的

📌 结论
如果你是 Scala / ZIO 框架,这是好方案
不适合你现在这个 Koa 风格 + Kotlin 的设计

四、方案三:Context-level override(⭐ 可用,但不够优雅)

做法

Request 仍不可变,但 Context 提供 override 字段:

class Context(
    val request: Request,
) {
    var overrideMethod: String? = null
    var overridePath: String? = null
}

访问时统一走:

fun Context.method() = overrideMethod ?: request.method
fun Context.path() = overridePath ?: request.path

middleware:

ctx.overrideMethod = "PUT"
ctx.overridePath = "/users/1"

优点

缺点(关键)

  1. override 语义分散

    • Router / Logger / OpenAPI 都要“记得用 ctx.method()”

    • 非常容易漏

  2. 扩展性差

    • 以后加 host rewrite / scheme rewrite?

    • Context 变成一堆 nullable override

📌 结论
这是一个 过渡方案,不是长期架构。

五、方案四(我最推荐):Request 分层模型(🏆 最优雅)

核心思想

区分「事实」和「视图」

1️⃣ RawRequest(只读,永远不变)

class RawRequest(
    val method: String,
    val path: String,
    val queryString: String,
    val headers: Headers,
    ...
)

2️⃣ RequestView(可变、轻量)

class RequestView(
    val raw: RawRequest
) {
    var method: String = raw.method
    var path: String = raw.path
    var queryString: String = raw.queryString
}

3️⃣ Context 暴露的是 View

class Context(
    val rawRequest: RawRequest,
) {
    val request: RequestView = RequestView(rawRequest)
}

middleware:

app.use { ctx, next ->
    if (ctx.request.method == "POST" && ctx.request.query["_method"] != null) {
        ctx.request.method = ctx.request.query["_method"]!!
    }

    ctx.request.path = rewritePath(ctx.request.path)
    next()
}

为什么这是“框架级优雅方案”

✅ 1. 语义非常清晰

概念

是否可变

用途

RawRequest

logging / audit / debug

RequestView

routing / handler

✅ 2. Router 天然支持 rewrite

Router 只看:

ctx.request.method
ctx.request.path

无需任何特殊判断。

✅ 3. 中间件顺序 = 语义顺序(可控)

methodOverrideMiddleware
→ pathRewriteMiddleware
→ router

这是人类直觉

✅ 4. 为未来功能天然留好接口

你未来几乎一定会加:

这个模型 一次性解决

六、Method Override & Path Rewrite 的推荐实现方式

Method Override(HTML Form 经典需求)

app.use { ctx, next ->
    val override = ctx.request.query["_method"]
        ?: ctx.request.headers["X-HTTP-Method-Override"]

    if (override != null) {
        ctx.request.method = override.uppercase()
    }
    next()
}

Path Rewrite(mount / proxy / alias)

app.use { ctx, next ->
    if (ctx.request.path.startsWith("/api")) {
        ctx.request.path = ctx.request.path.removePrefix("/api")
    }
    next()
}

七、我的最终建议(非常明确)

不要让 middleware 修改 Request 本身
也不要让 override 零散存在 Context
而是引入 Request 的「视图层」

这是一个:

的设计。

如果你愿意,下一步我可以:

你这个问题问得非常对 ——这是“作者级”问题。