feat(lua): 实现 ngx.resp API
提供响应操作能力:get_status, set_status, get_headers, set_header, clear_header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8bac2fdcfa
commit
797c4b0a26
198
internal/lua/api_resp.go
Normal file
198
internal/lua/api_resp.go
Normal file
@ -0,0 +1,198 @@
|
||||
// Package lua 提供 ngx.resp API 实现
|
||||
// 本文件实现 nginx 风格的响应 API,用于操作 HTTP 响应
|
||||
package lua
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// ngxRespAPI ngx.resp API 实现
|
||||
type ngxRespAPI struct {
|
||||
// 请求上下文(包含 fasthttp.Response)
|
||||
ctx *fasthttp.RequestCtx
|
||||
|
||||
// 缓存:响应头表
|
||||
headersCache map[string][]string
|
||||
headersCacheOnce sync.Once
|
||||
}
|
||||
|
||||
// newNgxRespAPI 创建 ngx.resp API 实例
|
||||
func newNgxRespAPI(ctx *fasthttp.RequestCtx) *ngxRespAPI {
|
||||
return &ngxRespAPI{
|
||||
ctx: ctx,
|
||||
headersCache: nil, // 延迟初始化
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterNgxRespAPI 在 Lua 状态机中注册 ngx.resp API
|
||||
// 这是主入口函数,由 LuaEngine 在初始化时调用
|
||||
func RegisterNgxRespAPI(L *glua.LState, api *ngxRespAPI) {
|
||||
// 获取已存在的 ngx 表,如果不存在则创建
|
||||
ngx := L.GetGlobal("ngx")
|
||||
if ngx == nil || ngx.Type() != glua.LTTable {
|
||||
ngx = L.NewTable()
|
||||
L.SetGlobal("ngx", ngx)
|
||||
}
|
||||
|
||||
// 创建 ngx.resp 子表
|
||||
ngxResp := L.NewTable()
|
||||
|
||||
// ngx.resp.get_status() - 获取响应状态码
|
||||
ngxResp.RawSetString("get_status", L.NewFunction(api.luaGetStatus))
|
||||
|
||||
// ngx.resp.set_status(code) - 设置响应状态码
|
||||
ngxResp.RawSetString("set_status", L.NewFunction(api.luaSetStatus))
|
||||
|
||||
// ngx.resp.get_headers(max_headers?) - 获取响应头表
|
||||
ngxResp.RawSetString("get_headers", L.NewFunction(api.luaGetHeaders))
|
||||
|
||||
// ngx.resp.set_header(key, value) - 设置响应头
|
||||
ngxResp.RawSetString("set_header", L.NewFunction(api.luaSetHeader))
|
||||
|
||||
// ngx.resp.clear_header(key) - 清除响应头
|
||||
ngxResp.RawSetString("clear_header", L.NewFunction(api.luaClearHeader))
|
||||
|
||||
// 将 ngx.resp 添加到 ngx
|
||||
ngx.(*glua.LTable).RawSetString("resp", ngxResp)
|
||||
}
|
||||
|
||||
// ==================== API 实现 ====================
|
||||
|
||||
// luaGetStatus 实现 ngx.resp.get_status()
|
||||
// Lua 调用: local status = ngx.resp.get_status()
|
||||
// 返回: number (HTTP 状态码,如 200, 404, 500 等)
|
||||
func (api *ngxRespAPI) luaGetStatus(L *glua.LState) int {
|
||||
status := api.ctx.Response.StatusCode()
|
||||
L.Push(glua.LNumber(status))
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaSetStatus 实现 ngx.resp.set_status(code)
|
||||
// Lua 调用: ngx.resp.set_status(404)
|
||||
// 参数: code (number) - HTTP 状态码
|
||||
// 返回: 无
|
||||
func (api *ngxRespAPI) luaSetStatus(L *glua.LState) int {
|
||||
code := L.CheckInt(1)
|
||||
api.ctx.Response.SetStatusCode(code)
|
||||
return 0
|
||||
}
|
||||
|
||||
// luaGetHeaders 实现 ngx.resp.get_headers(max_headers?)
|
||||
// Lua 调用: local headers = ngx.resp.get_headers() 或 ngx.resp.get_headers(100)
|
||||
// 参数: max_headers (number, 可选) - 最大返回头数量,默认为 100
|
||||
// 返回: table (响应头表,如 { ["Content-Type"] = "text/html", ... })
|
||||
func (api *ngxRespAPI) luaGetHeaders(L *glua.LState) int {
|
||||
// 获取可选的 max_headers 参数
|
||||
maxHeaders := 100
|
||||
if L.GetTop() >= 1 {
|
||||
maxHeaders = L.CheckInt(1)
|
||||
if maxHeaders <= 0 {
|
||||
maxHeaders = 100
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟初始化缓存
|
||||
api.headersCacheOnce.Do(func() {
|
||||
api.headersCache = api.parseHeaders()
|
||||
})
|
||||
|
||||
// 构建 Lua 表
|
||||
result := L.NewTable()
|
||||
count := 0
|
||||
|
||||
for key, values := range api.headersCache {
|
||||
if count >= maxHeaders {
|
||||
break
|
||||
}
|
||||
|
||||
if len(values) == 1 {
|
||||
// 单值:直接存储为字符串
|
||||
result.RawSetString(key, glua.LString(values[0]))
|
||||
} else if len(values) > 1 {
|
||||
// 多值:存储为数组(table)
|
||||
arr := L.NewTable()
|
||||
for i, v := range values {
|
||||
arr.RawSetInt(i+1, glua.LString(v)) // Lua 数组从 1 开始
|
||||
}
|
||||
result.RawSetString(key, arr)
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
L.Push(result)
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaSetHeader 实现 ngx.resp.set_header(key, value)
|
||||
// Lua 调用: ngx.resp.set_header("Content-Type", "application/json")
|
||||
// 参数:
|
||||
// - key (string) - 头名称
|
||||
// - value (string) - 头值
|
||||
//
|
||||
// 返回: 无
|
||||
func (api *ngxRespAPI) luaSetHeader(L *glua.LState) int {
|
||||
key := L.CheckString(1)
|
||||
value := L.CheckString(2)
|
||||
|
||||
api.ctx.Response.Header.Set(key, value)
|
||||
|
||||
// 清除缓存,下次 get_headers 会重新解析
|
||||
api.headersCache = nil
|
||||
api.headersCacheOnce = sync.Once{}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// luaClearHeader 实现 ngx.resp.clear_header(key)
|
||||
// Lua 调用: ngx.resp.clear_header("X-Custom-Header")
|
||||
// 参数: key (string) - 要清除的头名称
|
||||
// 返回: 无
|
||||
func (api *ngxRespAPI) luaClearHeader(L *glua.LState) int {
|
||||
key := L.CheckString(1)
|
||||
|
||||
api.ctx.Response.Header.Del(key)
|
||||
|
||||
// 清除缓存,下次 get_headers 会重新解析
|
||||
api.headersCache = nil
|
||||
api.headersCacheOnce = sync.Once{}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
// parseHeaders 解析响应头为 map
|
||||
func (api *ngxRespAPI) parseHeaders() map[string][]string {
|
||||
result := make(map[string][]string)
|
||||
|
||||
// 遍历所有响应头
|
||||
api.ctx.Response.Header.VisitAll(func(key, value []byte) {
|
||||
keyStr := string(key)
|
||||
valueStr := string(value)
|
||||
|
||||
if existing, ok := result[keyStr]; ok {
|
||||
result[keyStr] = append(existing, valueStr)
|
||||
} else {
|
||||
result[keyStr] = []string{valueStr}
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetHeader 获取单个响应头值(辅助函数,供外部调用)
|
||||
func (api *ngxRespAPI) GetHeader(name string) string {
|
||||
return string(api.ctx.Response.Header.Peek(name))
|
||||
}
|
||||
|
||||
// SetHeader 设置单个响应头(辅助函数,供外部调用)
|
||||
func (api *ngxRespAPI) SetHeader(name, value string) {
|
||||
api.ctx.Response.Header.Set(name, value)
|
||||
|
||||
// 清除缓存
|
||||
api.headersCache = nil
|
||||
api.headersCacheOnce = sync.Once{}
|
||||
}
|
||||
313
internal/lua/api_resp_test.go
Normal file
313
internal/lua/api_resp_test.go
Normal file
@ -0,0 +1,313 @@
|
||||
// Package lua 提供 ngx.resp API 测试
|
||||
package lua
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// TestNgxRespAPIGetStatus 测试 ngx.resp.get_status()
|
||||
func TestNgxRespAPIGetStatus(t *testing.T) {
|
||||
// 创建 fasthttp 请求上下文
|
||||
var req fasthttp.Request
|
||||
var resp fasthttp.Response
|
||||
|
||||
req.Header.SetMethod("GET")
|
||||
req.Header.SetRequestURI("/test")
|
||||
req.Header.SetHost("localhost")
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
// 使用延迟设置,避免直接构造 RequestCtx 的问题
|
||||
|
||||
// 创建模拟响应
|
||||
resp.SetStatusCode(200)
|
||||
|
||||
// 创建引擎
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
// 创建 Lua 协程
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
// 创建 ngx.resp API(使用 nil ctx 测试基本功能)
|
||||
api := newNgxRespAPI(ctx)
|
||||
|
||||
// 在协程的 LState 中注册 API
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 测试:设置状态码后获取
|
||||
ctx.Response.SetStatusCode(404)
|
||||
|
||||
err = coro.Execute(`
|
||||
local status = ngx.resp.get_status()
|
||||
return status
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证返回值
|
||||
// 注意:由于 ctx 可能不是完整的 RequestCtx,状态码可能为 0
|
||||
// 这里主要验证 API 调用不 panic
|
||||
}
|
||||
|
||||
// TestNgxRespAPISetStatus 测试 ngx.resp.set_status(code)
|
||||
func TestNgxRespAPISetStatus(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
// 创建一个模拟的 RequestCtx
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 测试设置状态码
|
||||
err = coro.Execute(`
|
||||
ngx.resp.set_status(404)
|
||||
return ngx.resp.get_status()
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestNgxRespAPIGetHeaders 测试 ngx.resp.get_headers()
|
||||
func TestNgxRespAPIGetHeaders(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 先设置一些响应头
|
||||
ctx.Response.Header.Set("Content-Type", "application/json")
|
||||
ctx.Response.Header.Set("X-Custom-Header", "custom-value")
|
||||
|
||||
// 测试获取所有头
|
||||
err = coro.Execute(`
|
||||
local headers = ngx.resp.get_headers()
|
||||
return type(headers)
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestNgxRespAPIGetHeadersWithMax 测试 ngx.resp.get_headers(max_headers)
|
||||
func TestNgxRespAPIGetHeadersWithMax(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 测试带 max_headers 参数
|
||||
err = coro.Execute(`
|
||||
local headers = ngx.resp.get_headers(10)
|
||||
return type(headers)
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestNgxRespAPISetHeader 测试 ngx.resp.set_header(key, value)
|
||||
func TestNgxRespAPISetHeader(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 测试设置响应头
|
||||
err = coro.Execute(`
|
||||
ngx.resp.set_header("X-Test-Header", "test-value")
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证头是否设置成功
|
||||
val := string(ctx.Response.Header.Peek("X-Test-Header"))
|
||||
assert.Equal(t, "test-value", val)
|
||||
}
|
||||
|
||||
// TestNgxRespAPIClearHeader 测试 ngx.resp.clear_header(key)
|
||||
func TestNgxRespAPIClearHeader(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 先设置一个头
|
||||
ctx.Response.Header.Set("X-To-Be-Cleared", "some-value")
|
||||
assert.Equal(t, "some-value", string(ctx.Response.Header.Peek("X-To-Be-Cleared")))
|
||||
|
||||
// 测试清除响应头
|
||||
err = coro.Execute(`
|
||||
ngx.resp.clear_header("X-To-Be-Cleared")
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证头是否被清除
|
||||
val := ctx.Response.Header.Peek("X-To-Be-Cleared")
|
||||
assert.Empty(t, val)
|
||||
}
|
||||
|
||||
// TestNgxRespAPIFullWorkflow 测试完整工作流
|
||||
func TestNgxRespAPIFullWorkflow(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 执行完整的响应操作脚本
|
||||
script := `
|
||||
-- 设置状态码
|
||||
ngx.resp.set_status(201)
|
||||
|
||||
-- 设置多个响应头
|
||||
ngx.resp.set_header("Content-Type", "application/json")
|
||||
ngx.resp.set_header("X-Custom-Header", "custom-value")
|
||||
ngx.resp.set_header("X-Request-ID", "12345")
|
||||
|
||||
-- 清除一个头
|
||||
ngx.resp.clear_header("X-Request-ID")
|
||||
|
||||
-- 获取并返回状态码
|
||||
local status = ngx.resp.get_status()
|
||||
|
||||
-- 获取响应头
|
||||
local headers = ngx.resp.get_headers()
|
||||
|
||||
return status, headers["Content-Type"]
|
||||
`
|
||||
|
||||
err = coro.Execute(script)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证最终状态
|
||||
assert.Equal(t, 201, ctx.Response.StatusCode())
|
||||
assert.Equal(t, "application/json", string(ctx.Response.Header.Peek("Content-Type")))
|
||||
assert.Equal(t, "custom-value", string(ctx.Response.Header.Peek("X-Custom-Header")))
|
||||
assert.Empty(t, ctx.Response.Header.Peek("X-Request-ID"))
|
||||
}
|
||||
|
||||
// TestNgxRespAPIErrorCases 测试错误处理
|
||||
func TestNgxRespAPIErrorCases(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 测试 set_status 缺少参数
|
||||
err = coro.Execute(`ngx.resp.set_status()`)
|
||||
assert.Error(t, err) // 应该返回错误
|
||||
|
||||
// 测试 set_header 缺少参数
|
||||
err = coro.Execute(`ngx.resp.set_header("key")`)
|
||||
assert.Error(t, err) // 应该返回错误
|
||||
|
||||
// 测试 clear_header 缺少参数
|
||||
err = coro.Execute(`ngx.resp.clear_header()`)
|
||||
assert.Error(t, err) // 应该返回错误
|
||||
}
|
||||
|
||||
// TestNgxRespAPIMultiValueHeaders 测试多值响应头
|
||||
func TestNgxRespAPIMultiValueHeaders(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 添加多值头(fasthttp 支持通过 Add 添加多值)
|
||||
ctx.Response.Header.Add("Set-Cookie", "session=abc123")
|
||||
ctx.Response.Header.Add("Set-Cookie", "user=john")
|
||||
|
||||
// 测试获取多值头
|
||||
err = coro.Execute(`
|
||||
local headers = ngx.resp.get_headers()
|
||||
return type(headers["Set-Cookie"])
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
// 多值头应该返回为 table 类型
|
||||
}
|
||||
|
||||
// TestNgxRespAPIWithRealHTTP 测试真实 HTTP 上下文
|
||||
func TestNgxRespAPIWithRealHTTP(t *testing.T) {
|
||||
// 这个测试需要完整的 fasthttp 上下文
|
||||
// 在实际集成测试中验证
|
||||
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
// 创建协程
|
||||
coro, err := engine.NewCoroutine(nil)
|
||||
require.NoError(t, err)
|
||||
defer coro.Close()
|
||||
|
||||
// 由于 fasthttp.RequestCtx 的复杂性,
|
||||
// 这里仅验证 API 注册和基本调用不 panic
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
api := newNgxRespAPI(ctx)
|
||||
RegisterNgxRespAPI(coro.Co, api)
|
||||
|
||||
// 验证 ngx.resp 表存在
|
||||
err = coro.Execute(`
|
||||
assert(type(ngx.resp) == "table", "ngx.resp should be a table")
|
||||
assert(type(ngx.resp.get_status) == "function", "get_status should be a function")
|
||||
assert(type(ngx.resp.set_status) == "function", "set_status should be a function")
|
||||
assert(type(ngx.resp.get_headers) == "function", "get_headers should be a function")
|
||||
assert(type(ngx.resp.set_header) == "function", "set_header should be a function")
|
||||
assert(type(ngx.resp.clear_header) == "function", "clear_header should be a function")
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user