http.go

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