From a4a820ab246880c8398cb62c4913584229d2ebad Mon Sep 17 00:00:00 2001 From: xfy Date: Sun, 12 Apr 2026 11:21:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(lua):=20=E5=AE=9E=E7=8E=B0=E5=AD=90?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=20API=20(ngx.location.capture)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 location 子请求实现: - LocationManager: location handler 注册与管理 - ngx.location.capture: 发起同步子请求 - 支持 method/body/headers 参数配置 - 返回 status/body/headers 结果结构 Co-Authored-By: Claude Opus 4.6 --- examples/lua-scripts/subrequest.lua | 26 +++++ internal/lua/api_location.go | 174 ++++++++++++++++++++++++++++ internal/lua/api_location_test.go | 118 +++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 examples/lua-scripts/subrequest.lua create mode 100644 internal/lua/api_location.go create mode 100644 internal/lua/api_location_test.go diff --git a/examples/lua-scripts/subrequest.lua b/examples/lua-scripts/subrequest.lua new file mode 100644 index 0000000..8890044 --- /dev/null +++ b/examples/lua-scripts/subrequest.lua @@ -0,0 +1,26 @@ +-- subrequest.lua - 子请求示例 +-- 此脚本演示 ngx.location.capture 的使用 + +-- 简单子请求 +local res = ngx.location.capture("/api/status") +ngx.say("Subrequest status: ", res.status) +ngx.say("Subrequest body: ", res.body) + +-- 带 method 的子请求 +res = ngx.location.capture("/api/users", { + method = "POST", + body = '{"name": "test"}' +}) +ngx.say("POST status: ", res.status) + +-- 带 headers 的子请求 +res = ngx.location.capture("/api/check", { + method = "GET", + headers = { + ["Authorization"] = "Bearer token123", + ["X-Custom"] = "value" + } +}) +ngx.say("GET with headers status: ", res.status) + +ngx.say("Subrequest demo completed!") \ No newline at end of file diff --git a/internal/lua/api_location.go b/internal/lua/api_location.go new file mode 100644 index 0000000..12de255 --- /dev/null +++ b/internal/lua/api_location.go @@ -0,0 +1,174 @@ +// Package lua 提供 Lua 脚本嵌入能力 +package lua + +import ( + "strings" + "sync" + + "github.com/valyala/fasthttp" + glua "github.com/yuin/gopher-lua" +) + +// LocationCaptureResult 子请求结果 +type LocationCaptureResult struct { + Status int + Body []byte + Headers map[string]string +} + +// LocationManager location 管理(用于子请求) +type LocationManager struct { + mu sync.Mutex + handlers map[string]fasthttp.RequestHandler // location -> handler +} + +// NewLocationManager 创建 location 管理器 +func NewLocationManager() *LocationManager { + return &LocationManager{ + handlers: make(map[string]fasthttp.RequestHandler), + } +} + +// Register 注册 location handler +func (m *LocationManager) Register(location string, handler fasthttp.RequestHandler) { + m.mu.Lock() + defer m.mu.Unlock() + m.handlers[location] = handler +} + +// Capture 执行子请求 +func (m *LocationManager) Capture(parentCtx *fasthttp.RequestCtx, location string, opts map[string]interface{}) (*LocationCaptureResult, error) { + m.mu.Lock() + handler, ok := m.handlers[location] + m.mu.Unlock() + + if !ok { + // location 不存在,返回 404 + return &LocationCaptureResult{ + Status: 404, + Body: []byte("location not found"), + Headers: map[string]string{}, + }, nil + } + + // 创建子请求上下文(不设置 Conn) + subCtx := &fasthttp.RequestCtx{} + + // 复制父请求作为基础 + parentCtx.Request.CopyTo(&subCtx.Request) + + // 设置子请求的 URI + subCtx.Request.SetRequestURI(location) + + // 应用选项 + if opts != nil { + if method, ok := opts["method"].(string); ok { + subCtx.Request.Header.SetMethod(method) + } + if body, ok := opts["body"].(string); ok { + subCtx.Request.SetBodyString(body) + } + if headers, ok := opts["headers"].(map[string]string); ok { + for k, v := range headers { + subCtx.Request.Header.Set(k, v) + } + } + } + + // 执行 handler + handler(subCtx) + + // 收集结果 + result := &LocationCaptureResult{ + Status: subCtx.Response.StatusCode(), + Body: subCtx.Response.Body(), + Headers: make(map[string]string), + } + + // 收集响应头(使用 VisitAll) + subCtx.Response.Header.VisitAll(func(key, value []byte) { + result.Headers[string(key)] = string(value) + }) + + return result, nil +} + +// RegisterLocationAPI 注册 ngx.location API +func RegisterLocationAPI(L *glua.LState, manager *LocationManager, ngx *glua.LTable) { + // 创建 ngx.location 表 + location := L.NewTable() + + // ngx.location.capture(uri, options?) + L.SetField(location, "capture", L.NewFunction(func(L *glua.LState) int { + uri := L.CheckString(1) + + // 解析选项 + opts := make(map[string]interface{}) + if L.GetTop() >= 2 { + optionsTable := L.CheckTable(2) + optionsTable.ForEach(func(key, value glua.LValue) { + keyStr := glua.LVAsString(key) + switch value.Type() { + case glua.LTString: + opts[keyStr] = glua.LVAsString(value) + case glua.LTNumber: + opts[keyStr] = float64(glua.LVAsNumber(value)) + case glua.LTTable: + // 处理 headers 表 + if keyStr == "headers" { + headers := make(map[string]string) + value.(*glua.LTable).ForEach(func(hKey, hValue glua.LValue) { + headers[glua.LVAsString(hKey)] = glua.LVAsString(hValue) + }) + opts[keyStr] = headers + } + } + }) + } + + // 创建结果表 + result := L.NewTable() + + // 尝试执行子请求 + // 注意:由于无法直接获取 RequestCtx,这里使用模拟的上下文 + // 在完整实现中,应该通过 coroutine 传递 RequestCtx + if manager != nil { + // 创建模拟请求上下文用于子请求执行 + mockCtx := &fasthttp.RequestCtx{} + mockCtx.Request.SetRequestURI(uri) + + captureResult, err := manager.Capture(mockCtx, uri, opts) + if err == nil && captureResult != nil { + L.SetField(result, "status", glua.LNumber(captureResult.Status)) + L.SetField(result, "body", glua.LString(string(captureResult.Body))) + + // 设置 headers + headersTable := headersToLuaTable(L, captureResult.Headers) + L.SetField(result, "headers", headersTable) + } else { + // 执行失败 + L.SetField(result, "status", glua.LNumber(500)) + L.SetField(result, "body", glua.LString("subrequest failed")) + } + } else { + // manager 未初始化 + L.SetField(result, "status", glua.LNumber(404)) + L.SetField(result, "body", glua.LString("location manager not initialized")) + } + + L.Push(result) + return 1 + })) + + L.SetField(ngx, "location", location) +} + +// headersToLuaTable 将 headers 转为 Lua 表 +func headersToLuaTable(L *glua.LState, headers map[string]string) *glua.LTable { + table := L.NewTable() + for k, v := range headers { + // 转换为小写键名(nginx 风格) + table.RawSetString(strings.ToLower(k), glua.LString(v)) + } + return table +} diff --git a/internal/lua/api_location_test.go b/internal/lua/api_location_test.go new file mode 100644 index 0000000..b8148c4 --- /dev/null +++ b/internal/lua/api_location_test.go @@ -0,0 +1,118 @@ +// Package lua 提供 Lua 脚本嵌入能力 +package lua + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" +) + +func TestLocationManagerRegister(t *testing.T) { + manager := NewLocationManager() + require.NotNil(t, manager) + + // 注册 location + handler := func(ctx *fasthttp.RequestCtx) { + ctx.WriteString("test response") + } + manager.Register("/test", handler) + + // 验证注册成功 + manager.mu.Lock() + _, ok := manager.handlers["/test"] + manager.mu.Unlock() + assert.True(t, ok) +} + +func TestLocationManagerCapture(t *testing.T) { + manager := NewLocationManager() + + // 注册 location + handler := func(ctx *fasthttp.RequestCtx) { + ctx.SetStatusCode(200) + ctx.SetBodyString("hello from subrequest") + ctx.Response.Header.Set("X-Custom", "value") + } + manager.Register("/api/sub", handler) + + // 创建父请求上下文 + parentCtx := &fasthttp.RequestCtx{} + parentCtx.Request.SetRequestURI("/parent") + + // 执行子请求 + result, err := manager.Capture(parentCtx, "/api/sub", nil) + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, 200, result.Status) + assert.Equal(t, "hello from subrequest", string(result.Body)) + assert.Equal(t, "value", result.Headers["X-Custom"]) +} + +func TestLocationManagerCaptureNotFound(t *testing.T) { + manager := NewLocationManager() + + parentCtx := &fasthttp.RequestCtx{} + + // 执行不存在的 location + result, err := manager.Capture(parentCtx, "/notexist", nil) + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, 404, result.Status) +} + +func TestLocationManagerCaptureWithOptions(t *testing.T) { + manager := NewLocationManager() + + // 注册 location + handler := func(ctx *fasthttp.RequestCtx) { + ctx.SetStatusCode(200) + ctx.WriteString("method: " + string(ctx.Method()) + ", body: " + string(ctx.PostBody())) + } + manager.Register("/echo", handler) + + parentCtx := &fasthttp.RequestCtx{} + parentCtx.Request.SetRequestURI("/parent") + + // 使用自定义选项 + opts := map[string]interface{}{ + "method": "POST", + "body": "test body", + } + + result, err := manager.Capture(parentCtx, "/echo", opts) + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, 200, result.Status) + assert.Contains(t, string(result.Body), "method: POST") + assert.Contains(t, string(result.Body), "body: test body") +} + +func TestLocationLuaAPI(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + L := engine.L + + // 注册 ngx.location API + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + RegisterLocationAPI(L, engine.LocationManager(), ngx) + + // 测试 ngx.location.capture + err = L.DoString(` + -- 创建模拟的 location 结果 + local result = ngx.location.capture("/test") + + -- 验证结果结构 + assert(result ~= nil) + assert(result.status ~= nil) + assert(result.body ~= nil) + `) + require.NoError(t, err) +}