feat(lua): 实现子请求 API (ngx.location.capture)

添加 location 子请求实现:
- LocationManager: location handler 注册与管理
- ngx.location.capture: 发起同步子请求
- 支持 method/body/headers 参数配置
- 返回 status/body/headers 结果结构

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-12 11:21:32 +08:00
parent 026302465d
commit a4a820ab24
3 changed files with 318 additions and 0 deletions

View File

@ -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!")

View File

@ -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
}

View File

@ -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)
}