http.go

  1package plugin
  2
  3import (
  4	"io"
  5	"net/http"
  6	"strings"
  7
  8	lua "github.com/yuin/gopher-lua"
  9
 10	"github.com/floatpane/matcha/internal/httpclient"
 11)
 12
 13const httpMaxBodySize = 1 << 20 // 1 MB
 14
 15var httpClient = httpclient.New(httpclient.PluginCallTimeout)
 16
 17// luaHTTP implements matcha.http(options) — make an HTTP request.
 18//
 19// options is a table with fields:
 20//   - url     (string, required)
 21//   - method  (string, optional, default "GET")
 22//   - headers (table, optional)
 23//   - body    (string, optional)
 24//
 25// Returns (response_table, nil) on success or (nil, error_string) on failure.
 26// response_table has fields: status (number), body (string), headers (table).
 27func (m *Manager) luaHTTP(L *lua.LState) int {
 28	opts := L.CheckTable(1)
 29
 30	// URL (required).
 31	urlVal := opts.RawGetString("url")
 32	if urlVal == lua.LNil {
 33		L.Push(lua.LNil)
 34		L.Push(lua.LString("missing required field: url"))
 35		return 2
 36	}
 37	rawURL := urlVal.String()
 38
 39	// Scheme validation.
 40	if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
 41		L.Push(lua.LNil)
 42		L.Push(lua.LString("unsupported URL scheme: only http and https are allowed"))
 43		return 2
 44	}
 45
 46	// Method (optional, default GET).
 47	method := "GET"
 48	if v := opts.RawGetString("method"); v != lua.LNil {
 49		method = strings.ToUpper(v.String())
 50	}
 51
 52	// Body (optional).
 53	var bodyReader io.Reader
 54	if v := opts.RawGetString("body"); v != lua.LNil {
 55		bodyReader = strings.NewReader(v.String())
 56	}
 57
 58	req, err := http.NewRequest(method, rawURL, bodyReader)
 59	if err != nil {
 60		L.Push(lua.LNil)
 61		L.Push(lua.LString(err.Error()))
 62		return 2
 63	}
 64
 65	// Headers (optional).
 66	if v := opts.RawGetString("headers"); v != lua.LNil {
 67		if tbl, ok := v.(*lua.LTable); ok {
 68			tbl.ForEach(func(k, v lua.LValue) {
 69				req.Header.Set(k.String(), v.String())
 70			})
 71		}
 72	}
 73
 74	resp, err := httpClient.Do(req)
 75	if err != nil {
 76		L.Push(lua.LNil)
 77		L.Push(lua.LString(err.Error()))
 78		return 2
 79	}
 80	defer resp.Body.Close()
 81
 82	body, err := io.ReadAll(io.LimitReader(resp.Body, httpMaxBodySize))
 83	if err != nil {
 84		L.Push(lua.LNil)
 85		L.Push(lua.LString(err.Error()))
 86		return 2
 87	}
 88
 89	// Build response table.
 90	result := L.NewTable()
 91	result.RawSetString("status", lua.LNumber(resp.StatusCode))
 92	result.RawSetString("body", lua.LString(string(body)))
 93
 94	headers := L.NewTable()
 95	for k, vals := range resp.Header {
 96		if len(vals) > 0 {
 97			headers.RawSetString(strings.ToLower(k), lua.LString(vals[0]))
 98		}
 99	}
100	result.RawSetString("headers", headers)
101
102	L.Push(result)
103	L.Push(lua.LNil)
104	return 2
105}