Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions runtime/lua/modules/http/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ func TestRequest_Basic(t *testing.T) {
req := httptest.NewRequestWithContext(context.Background(), "GET", "/test", nil)
req.Header.Set("X-Custom-Header", "custom-value")
req.Header.Set("Authorization", "Bearer token123")
req.Header.Add("X-Multi-Value", "first")
req.Header.Add("X-Multi-Value", "second")
recorder := httptest.NewRecorder()
reqCtx := httpservice.NewRequestContext(req, recorder)
_ = fc.Set(httpservice.RequestKey(), reqCtx)
Expand All @@ -237,12 +239,41 @@ func TestRequest_Basic(t *testing.T) {
err := l.DoString(`
local req = http.request()
assert(req:header("X-Custom-Header") == "custom-value", "header should be custom-value")
assert(req:header("x-custom-header") == "custom-value", "header lookup should be case-insensitive")
assert(req:header("Authorization") == "Bearer token123", "auth header should match")
assert(req:header("x-multi-value") == "first, second", "multi-value headers should stay joined")
assert(req:header("Non-Existent") == nil, "missing header should be nil")
`)
assert.NoError(t, err)
})

t.Run("get all headers", func(t *testing.T) {
l := lua.NewState()
defer l.Close()
bind(l)

ctx, fc := newTestContext()
req := httptest.NewRequestWithContext(context.Background(), "GET", "/test", nil)
req.Header.Set("X-Custom-Header", "custom-value")
req.Header.Add("X-Multi-Value", "first")
req.Header.Add("X-Multi-Value", "second")
recorder := httptest.NewRecorder()
reqCtx := httpservice.NewRequestContext(req, recorder)
_ = fc.Set(httpservice.RequestKey(), reqCtx)
l.SetContext(ctx)

err := l.DoString(`
local req = http.request()
local headers = req:headers()
assert(headers["X-Custom-Header"] == "custom-value", "header should be present")
assert(headers["X-Multi-Value"] == "first, second", "multi-value headers should stay joined")
headers["X-Custom-Header"] = "changed"
assert(req:header("X-Custom-Header") == "custom-value", "headers should return a detached table")
`)
assert.NoError(t, err)
assert.Equal(t, "custom-value", req.Header.Get("X-Custom-Header"))
})

t.Run("get content type and length", func(t *testing.T) {
l := lua.NewState()
defer l.Close()
Expand Down
21 changes: 20 additions & 1 deletion runtime/lua/modules/http/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var requestMethods = map[string]lua.LGoFunc{
"query": requestQuery,
"query_params": requestQueryParams,
"header": requestHeader,
"headers": requestHeaders,
"content_type": requestContentType,
"content_length": requestContentLength,
"host": requestHost,
Expand Down Expand Up @@ -206,7 +207,7 @@ func requestHeader(l *lua.LState) int {
return 0
}
key := l.CheckString(2)
values := req.request.Header[key]
values := req.request.Header.Values(key)
if len(values) == 0 {
l.Push(lua.LNil)
l.Push(lua.LNil)
Expand All @@ -217,6 +218,24 @@ func requestHeader(l *lua.LState) int {
return 2
}

func requestHeaders(l *lua.LState) int {
req := checkRequest(l, 1)
if req == nil {
return 0
}

headers := l.CreateTable(0, len(req.request.Header))
for name, values := range req.request.Header {
if len(values) > 0 {
headers.RawSetString(name, lua.LString(strings.Join(values, ", ")))
}
}

l.Push(headers)
l.Push(lua.LNil)
return 2
}

func requestContentType(l *lua.LState) int {
req := checkRequest(l, 1)
if req == nil {
Expand Down
13 changes: 11 additions & 2 deletions runtime/lua/modules/http/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ Returned by `http.request()`. Provides access to incoming HTTP request data.
| path | () | string, error | Request path |
| query | (key: string) | string?, error | Single query parameter value |
| query_params | () | table, error | All query parameters as table |
| header | (name: string) | string?, error | Header value, multiple values joined with ", " |
| header | (name: string) | string?, error | Case-insensitive header value, multiple values joined with ", " |
| headers | () | table, error | All request headers as a detached table |
| content_type | () | string?, error | Content-Type header value |
| content_length | () | number, error | Content-Length as number |
| host | () | string, error | Host header value |
Expand Down Expand Up @@ -182,10 +183,18 @@ Returns request header value. Multiple header values are joined with ", ".

| Param | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| name | string | yes | - | Header name (case-sensitive) |
| name | string | yes | - | Header name (case-insensitive) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


**Returns:** `string?, error` - header value or nil if not present, plus error

#### request:headers() → table, error

Returns all incoming request headers as a detached table. Header names use
their canonical spelling and multiple values are joined with `", "`, matching
`request:header(name)`.

**Returns:** `table, error` - map of header names to values, plus error

#### request:content_type() → string?, error

Returns Content-Type header value.
Expand Down
1 change: 1 addition & 0 deletions runtime/lua/modules/http/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func init() {
{Name: "query", Type: typ.Func().Param("self", typ.Self).Param("key", typ.String).Returns(typ.NewOptional(typ.String), typ.NewOptional(typ.LuaError)).Build()},
{Name: "query_params", Type: typ.Func().Param("self", typ.Self).Returns(typ.NewMap(typ.String, typ.String), typ.NewOptional(typ.LuaError)).Build()},
{Name: "header", Type: typ.Func().Param("self", typ.Self).Param("name", typ.String).Returns(typ.NewOptional(typ.String), typ.NewOptional(typ.LuaError)).Build()},
{Name: "headers", Type: typ.Func().Param("self", typ.Self).Returns(typ.NewMap(typ.String, typ.String), typ.NewOptional(typ.LuaError)).Build()},
{Name: "content_type", Type: typ.Func().Param("self", typ.Self).Returns(typ.NewOptional(typ.String), typ.NewOptional(typ.LuaError)).Build()},
{Name: "content_length", Type: typ.Func().Param("self", typ.Self).Returns(typ.Number, typ.NewOptional(typ.LuaError)).Build()},
{Name: "host", Type: typ.Func().Param("self", typ.Self).Returns(typ.String, typ.NewOptional(typ.LuaError)).Build()},
Expand Down