Skip to content

请求

每个请求进入洋葱后,ACE 会构造一个 Context 对象并沿中间件链传递。Controller 方法可以通过注解直接绑定所需数据,也可以通过 ctx 对象手动读取。

Context 基础属性

在中间件或未使用宏的路由处理函数中,可通过 ctx 直接访问:

cangjie
app.use { ctx, next =>
    let method = ctx.method          // "GET" / "POST" / ...
    let path   = ctx.path            // "/api/tasks/42"
    let query  = ctx.query           // "page=1&size=20"
    let body   = ctx.rawBody()       // 原始请求体字节 Array<UInt8>
    let token  = ctx.header("x-token") ?? ""   // 读请求头(不区分大小写)
    let sid    = ctx.cookie("session_id") ?? "" // 读 Cookie
    await next(ctx)
}

ctx.params 在路由匹配后自动填充路径参数:

cangjie
router.get("/tasks/:id") { ctx, next =>
    let id = ctx.params["id"] ?? "unknown"
    ctx.json("{\"id\":\"${id}\"}")
}

参数绑定注解

@Controller 方法中,推荐使用注解自动绑定,ACE 在编译期生成提取和校验代码。

路径参数 @PathParam

cangjie
@Get["/:id"]
func get(@PathParam id: String): String {
    // id 已从 ctx.params["id"] 提取
    "task ${id}"
}

查询参数 @Query

写法含义缺失时
@Query["page"] page: Int64可选返回 None(需 Option<Int64>
@Query["page!"] page: Int64必填自动返回 400 Bad Request
cangjie
@Get[""]
func list(
    @Query["page"]  page: Option<Int64>,
    @Query["size!"] size: Int64          // 缺失则 400
): String {
    let p = page ?? 1
    "page=${p}, size=${size}"
}

表单参数 @Param

application/x-www-form-urlencodedmultipart/form-data 中提取字段:

cangjie
@Post["/login"]
func login(
    @Param["username!"] username: String,
    @Param["password!"] password: String
): String { ... }

必填标记 !

在键名末尾加 !(如 "username!")表示必填。参数缺失时框架自动返回 400 Bad Request,无需手动校验。

Body 反序列化

接收原始 JSON 字符串后手动解析

cangjie
import ace_framework_runtime.*   // parseJson / fieldStr / fieldInt64

@Post[""]
func create(@Body raw: String): String {
    let json = parseJson(raw)
    let title    = fieldStr(json, "title") ?? ""
    let priority = fieldInt64(json, "priority") ?? 0
    // ... 业务逻辑
    "{\"id\":\"new\"}"
}

使用 @Dto 宏自动反序列化

定义 DTO struct 并标注 @Dto,ACE 在编译期生成 fromJson 实现:

cangjie
@Dto
struct CreateTaskDto {
    let title: String
    let priority: Int64
    let tags: Array<String>
}

@Post[""]
func create(@Body dto: CreateTaskDto): String {
    // dto 已从请求体自动解析,字段类型不匹配时返回 400
    "created: ${dto.title}"
}

Content-Type 要求

@Body 自动反序列化要求请求头 Content-Type: application/json。发送表单数据请使用 @Param

文件上传

单文件 @File

cangjie
@Post["/upload"]
func upload(@File["avatar!"] avatar: UploadedFile): String {
    let name        = avatar.filename    // 原始文件名
    let bytes       = avatar.bytes       // Array<UInt8> 文件内容
    let contentType = avatar.contentType // "image/png" 等
    // 存储到本地或对象存储...
    "{\"size\":${bytes.size}}"
}

多文件 @Files

cangjie
@Post["/batch-upload"]
func batchUpload(@Files["docs"] docs: Array<UploadedFile>): String {
    for doc in docs {
        // 处理每个文件
        println("received: ${doc.filename}")
    }
    "{\"count\":${docs.size}}"
}

UploadedFile API

属性类型说明
filenameString客户端上传的原始文件名
bytesArray<UInt8>文件二进制内容
contentTypeStringMIME 类型,如 image/jpeg
sizeInt64文件字节大小

内存限制

UploadedFile.bytes 将整个文件加载到内存。上传大文件场景请在应用层做大小校验(如 avatar.size > 10 * 1024 * 1024 时返回 413),或等待后续流式上传 API。

Multipart 表单混合数据

同一请求中同时包含文本字段和文件时,文本字段仍用 @Param 绑定:

cangjie
@Post["/articles"]
func createArticle(
    @Param["title!"]   title:  String,
    @Param["content!"] content: String,
    @File["cover"]     cover:  Option<UploadedFile>
): String {
    let hasCover = cover.isSome()
    "title=${title}, hasCover=${hasCover}"
}

直接读取 ctx

在不使用宏的场景(如自定义中间件),可从 ctx 手动提取所有信息:

cangjie
// 读取所有查询参数
let queryStr = ctx.query   // "foo=1&bar=2"

// 读取请求头
let accept = ctx.header("accept") ?? "*/*"

// 读取 Cookie
let lang = ctx.cookie("lang") ?? "zh"

// 读取原始 Body(bytes)
let rawBytes = ctx.rawBody()
let bodyStr  = String.fromUtf8(rawBytes)

rawBody 只能读一次

ctx.rawBody() 返回完整请求体字节,ace-bodyparser 中间件执行后会将解析结果缓存到 ctx.state。若手动读取 rawBody,请在 bodyparser 中间件之前调用,避免重复消费。

基于 Apache-2.0 许可证发布