// 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]) // 偵測 JSON `{"error":"..."}` 模式視為失敗。 // 2026-06-09 修架構債:host function(.component-builds/http_request/src/index.ts)現在對非 2xx // 回 envelope `{"error":"HTTP ","status":,"body":<原文>}`——故此處 parsed["error"] // 能正確 catch 所有 4xx/5xx(含 Notion 401 那種 body 用 {"object":"error"} 不帶 error key 的)。 // 之前 host fn 只回 body 原文丟掉 status → 401 被判 success(系統假綠根因,已修)。 // 註:claude_api/kbdb_upsert_block/km_writer 已同樣修(非 2xx 回 error envelope)。 // auth_service_account 不套此 envelope——它 main.go 自己解析 OAuth token 回應的 // {access_token,error,error_description},access_token 空即視為失敗,已有自己的判定, // 套 envelope 反而會丟掉 error_description 破壞 token exchange 錯誤處理。 // 待辦:4 份 inline host fn 最好抽成共用 helper(dedup,目前複製貼上)。 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) }