// http_request — 發送任意 HTTP 請求,回傳 status + body // 透過 host function 發出 HTTP,.wasm 本身不含網路 syscall // //go:build tinygo package main import ( "encoding/json" "io" "os" "unsafe" ) // host function 宣告(由 WASI shim 注入) // //go:wasmimport u6u http_request func hostHttpRequest( urlPtr uintptr, urlLen uint32, methodPtr uintptr, methodLen uint32, headersPtr uintptr, headersLen uint32, bodyPtr uintptr, bodyLen uint32, outPtr uintptr, outLenPtr uintptr, ) uint32 type Input struct { URL string `json:"url"` Method string `json:"method"` Headers map[string]string `json:"headers"` Body string `json:"body"` // 模式 A:直接 string body BodyJSON map[string]interface{} `json:"body_json"` // 模式 B:物件,內部 stringify(避免 yaml 端要自己組 JSON 字串) } // dummy byte for safe zero-length unsafe.Pointer operations var dummy [1]byte // safePtr returns a valid pointer for an empty-or-nonempty byte slice. // TinyGo panics with "index out of range" when taking &b[0] on empty b. func safePtr(b []byte) (uintptr, uint32) { if len(b) == 0 { return uintptr(unsafe.Pointer(&dummy[0])), 0 } return uintptr(unsafe.Pointer(&b[0])), uint32(len(b)) } func main() { raw, err := io.ReadAll(os.Stdin) if err != nil { writeError("failed to read stdin: " + err.Error()) return } var input Input if err := json.Unmarshal(raw, &input); err != nil { writeError("invalid input JSON: " + err.Error()) return } if input.URL == "" { writeError("url 必填") return } method := input.Method if method == "" { method = "GET" } headersJSON := "{}" if len(input.Headers) > 0 { b, _ := json.Marshal(input.Headers) headersJSON = string(b) } // body 來源優先順序:body_json(物件 → JSON 字串)> body(直接 string) bodyStr := input.Body if input.BodyJSON != nil { b, err := json.Marshal(input.BodyJSON) if err == nil { bodyStr = string(b) } } urlBytes := []byte(input.URL) methodBytes := []byte(method) headersBytes := []byte(headersJSON) bodyBytes := []byte(bodyStr) outBuf := make([]byte, 65536) // 64KB output buffer var outLen uint32 urlPtr, urlLen := safePtr(urlBytes) methodPtr, methodLen := safePtr(methodBytes) headersPtr, headersLen := safePtr(headersBytes) bodyPtr, bodyLen := safePtr(bodyBytes) result := hostHttpRequest( urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), ) if result != 0 { writeError("HTTP request failed") return } responseStr := string(outBuf[:outLen]) // 2026-05-14:偵測 JSON `{"error":"..."}` 模式視為 4xx 失敗 // 理由:host function 沒回 HTTP status code(架構債),先用 body 啟發式 catch。 // 標準 API(cypher-executor / KBDB / 多數 REST)失敗時都回 {"error":...} JSON。 // 對應 SDD: arcrun.md 三-A P1 #4「http_request status code 缺乏 surface」。 var parsed map[string]interface{} if err := json.Unmarshal([]byte(responseStr), &parsed); err == nil { if errVal, ok := parsed["error"]; ok && errVal != nil { out, _ := json.Marshal(map[string]interface{}{ "success": false, "error": errVal, "data": map[string]interface{}{"body": responseStr}, }) os.Stdout.Write(out) return } } out, _ := json.Marshal(map[string]interface{}{ "success": true, "data": map[string]interface{}{"body": responseStr}, }) os.Stdout.Write(out) } func writeError(msg string) { out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg}) os.Stdout.Write(out) }