827 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			827 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
|  * Copyright 2025 coze-dev Authors
 | |
|  *
 | |
|  * Licensed under the Apache License, Version 2.0 (the "License");
 | |
|  * you may not use this file except in compliance with the License.
 | |
|  * You may obtain a copy of the License at
 | |
|  *
 | |
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | |
|  *
 | |
|  * Unless required by applicable law or agreed to in writing, software
 | |
|  * distributed under the License is distributed on an "AS IS" BASIS,
 | |
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|  * See the License for the specific language governing permissions and
 | |
|  * limitations under the License.
 | |
|  */
 | |
| 
 | |
| package httprequester
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"maps"
 | |
| 	"mime/multipart"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/convert"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/crypto"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/sonic"
 | |
| )
 | |
| 
 | |
| const defaultGetFileTimeout = 20       // second
 | |
| const maxSize int64 = 20 * 1024 * 1024 // 20MB
 | |
| 
 | |
| const (
 | |
| 	HeaderAuthorization = "Authorization"
 | |
| 	HeaderBearerPrefix  = "Bearer "
 | |
| 	HeaderContentType   = "Content-Type"
 | |
| )
 | |
| 
 | |
| type AuthType uint
 | |
| 
 | |
| const (
 | |
| 	BearToken AuthType = 1
 | |
| 	Custom    AuthType = 2
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	ContentTypeJSON           = "application/json"
 | |
| 	ContentTypePlainText      = "text/plain"
 | |
| 	ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
 | |
| 	ContentTypeBinary         = "application/octet-stream"
 | |
| )
 | |
| 
 | |
| type Location uint8
 | |
| 
 | |
| const (
 | |
| 	Header     Location = 1
 | |
| 	QueryParam Location = 2
 | |
| )
 | |
| 
 | |
| type BodyType string
 | |
| 
 | |
| const (
 | |
| 	BodyTypeNone           BodyType = "EMPTY"
 | |
| 	BodyTypeJSON           BodyType = "JSON"
 | |
| 	BodyTypeRawText        BodyType = "RAW_TEXT"
 | |
| 	BodyTypeFormData       BodyType = "FORM_DATA"
 | |
| 	BodyTypeFormURLEncoded BodyType = "FORM_URLENCODED"
 | |
| 	BodyTypeBinary         BodyType = "BINARY"
 | |
| )
 | |
| 
 | |
| type URLConfig struct {
 | |
| 	Tpl string `json:"tpl"`
 | |
| }
 | |
| 
 | |
| type IgnoreExceptionSetting struct {
 | |
| 	IgnoreException bool           `json:"ignore_exception"`
 | |
| 	DefaultOutput   map[string]any `json:"default_output,omitempty"`
 | |
| }
 | |
| 
 | |
| type BodyConfig struct {
 | |
| 	BodyType        BodyType         `json:"body_type"`
 | |
| 	FormDataConfig  *FormDataConfig  `json:"form_data_config,omitempty"`
 | |
| 	TextPlainConfig *TextPlainConfig `json:"text_plain_config,omitempty"`
 | |
| 	TextJsonConfig  *TextJsonConfig  `json:"text_json_config,omitempty"`
 | |
| }
 | |
| 
 | |
| type FormDataConfig struct {
 | |
| 	FileTypeMapping map[string]bool `json:"file_type_mapping"`
 | |
| }
 | |
| 
 | |
| type TextPlainConfig struct {
 | |
| 	Tpl string `json:"tpl"`
 | |
| }
 | |
| 
 | |
| type TextJsonConfig struct {
 | |
| 	Tpl string
 | |
| }
 | |
| 
 | |
| type AuthenticationConfig struct {
 | |
| 	Type     AuthType `json:"type"`
 | |
| 	Location Location `json:"location"`
 | |
| }
 | |
| 
 | |
| type Authentication struct {
 | |
| 	Key   string
 | |
| 	Value string
 | |
| 	Token string
 | |
| }
 | |
| 
 | |
| type Request struct {
 | |
| 	URLVars            map[string]any
 | |
| 	Headers            map[string]string
 | |
| 	Params             map[string]string
 | |
| 	Authentication     *Authentication
 | |
| 	FormDataVars       map[string]string
 | |
| 	FormURLEncodedVars map[string]string
 | |
| 	JsonVars           map[string]any
 | |
| 	TextPlainVars      map[string]any
 | |
| 	FileURL            *string
 | |
| }
 | |
| 
 | |
| var globalVariableReplaceRegexp = regexp.MustCompile(`global_variable_(\w+)\["(\w+)"]`)
 | |
| 
 | |
| type MD5FieldMapping struct {
 | |
| 	HeaderMD5Mapping map[string]string `json:"header_md_5_mapping,omitempty"` // md5 vs key
 | |
| 	ParamMD5Mapping  map[string]string `json:"param_md_5_mapping,omitempty"`
 | |
| 	URLMD5Mapping    map[string]string `json:"url_md_5_mapping,omitempty"`
 | |
| 	BodyMD5Mapping   map[string]string `json:"body_md_5_mapping,omitempty"`
 | |
| }
 | |
| 
 | |
| func (fm *MD5FieldMapping) SetHeaderFields(fields ...string) {
 | |
| 	if fm.HeaderMD5Mapping == nil && len(fields) > 0 {
 | |
| 		fm.HeaderMD5Mapping = make(map[string]string)
 | |
| 	}
 | |
| 	for _, field := range fields {
 | |
| 		fm.HeaderMD5Mapping[crypto.MD5HexValue(field)] = field
 | |
| 	}
 | |
| 
 | |
| }
 | |
| func (fm *MD5FieldMapping) SetParamFields(fields ...string) {
 | |
| 	if fm.ParamMD5Mapping == nil && len(fields) > 0 {
 | |
| 		fm.ParamMD5Mapping = make(map[string]string)
 | |
| 	}
 | |
| 
 | |
| 	for _, field := range fields {
 | |
| 		fm.ParamMD5Mapping[crypto.MD5HexValue(field)] = field
 | |
| 	}
 | |
| 
 | |
| }
 | |
| func (fm *MD5FieldMapping) SetURLFields(fields ...string) {
 | |
| 	if fm.URLMD5Mapping == nil && len(fields) > 0 {
 | |
| 		fm.URLMD5Mapping = make(map[string]string)
 | |
| 	}
 | |
| 	for _, field := range fields {
 | |
| 		fm.URLMD5Mapping[crypto.MD5HexValue(field)] = field
 | |
| 	}
 | |
| 
 | |
| }
 | |
| func (fm *MD5FieldMapping) SetBodyFields(fields ...string) {
 | |
| 	if fm.BodyMD5Mapping == nil && len(fields) > 0 {
 | |
| 		fm.BodyMD5Mapping = make(map[string]string)
 | |
| 	}
 | |
| 	for _, field := range fields {
 | |
| 		fm.BodyMD5Mapping[crypto.MD5HexValue(field)] = field
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| type Config struct {
 | |
| 	URLConfig  URLConfig
 | |
| 	AuthConfig *AuthenticationConfig
 | |
| 	BodyConfig BodyConfig
 | |
| 	Method     string
 | |
| 	Timeout    time.Duration
 | |
| 	RetryTimes uint64
 | |
| 
 | |
| 	MD5FieldMapping
 | |
| }
 | |
| 
 | |
| func (c *Config) Adapt(_ context.Context, n *vo.Node, opts ...nodes.AdaptOption) (*schema.NodeSchema, error) {
 | |
| 	options := nodes.GetAdaptOptions(opts...)
 | |
| 	if options.Canvas == nil {
 | |
| 		return nil, fmt.Errorf("canvas is requried when adapting HTTPRequester node")
 | |
| 	}
 | |
| 
 | |
| 	implicitDeps, err := extractImplicitDependency(n, options.Canvas)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	ns := &schema.NodeSchema{
 | |
| 		Key:     vo.NodeKey(n.ID),
 | |
| 		Type:    entity.NodeTypeHTTPRequester,
 | |
| 		Name:    n.Data.Meta.Title,
 | |
| 		Configs: c,
 | |
| 	}
 | |
| 
 | |
| 	inputs := n.Data.Inputs
 | |
| 
 | |
| 	md5FieldMapping := &MD5FieldMapping{}
 | |
| 
 | |
| 	method := inputs.APIInfo.Method
 | |
| 	c.Method = method
 | |
| 	reqURL := inputs.APIInfo.URL
 | |
| 	c.URLConfig = URLConfig{
 | |
| 		Tpl: strings.TrimSpace(reqURL),
 | |
| 	}
 | |
| 
 | |
| 	urlVars := extractBracesContent(reqURL)
 | |
| 	md5FieldMapping.SetURLFields(urlVars...)
 | |
| 
 | |
| 	md5FieldMapping.SetHeaderFields(slices.Transform(inputs.Headers, func(a *vo.Param) string {
 | |
| 		return a.Name
 | |
| 	})...)
 | |
| 
 | |
| 	md5FieldMapping.SetParamFields(slices.Transform(inputs.Params, func(a *vo.Param) string {
 | |
| 		return a.Name
 | |
| 	})...)
 | |
| 
 | |
| 	if inputs.Auth != nil && inputs.Auth.AuthOpen {
 | |
| 		auth := &AuthenticationConfig{}
 | |
| 		ty, err := convertAuthType(inputs.Auth.AuthType)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		auth.Type = ty
 | |
| 		location, err := convertLocation(inputs.Auth.AuthData.CustomData.AddTo)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		auth.Location = location
 | |
| 
 | |
| 		c.AuthConfig = auth
 | |
| 	}
 | |
| 
 | |
| 	bodyConfig := BodyConfig{}
 | |
| 
 | |
| 	bodyConfig.BodyType = BodyType(inputs.Body.BodyType)
 | |
| 	switch BodyType(inputs.Body.BodyType) {
 | |
| 	case BodyTypeJSON:
 | |
| 		jsonTpl := inputs.Body.BodyData.Json
 | |
| 		bodyConfig.TextJsonConfig = &TextJsonConfig{
 | |
| 			Tpl: jsonTpl,
 | |
| 		}
 | |
| 		jsonVars := extractBracesContent(jsonTpl)
 | |
| 		md5FieldMapping.SetBodyFields(jsonVars...)
 | |
| 	case BodyTypeFormData:
 | |
| 		bodyConfig.FormDataConfig = &FormDataConfig{
 | |
| 			FileTypeMapping: map[string]bool{},
 | |
| 		}
 | |
| 		formDataVars := make([]string, 0)
 | |
| 		for i := range inputs.Body.BodyData.FormData.Data {
 | |
| 			p := inputs.Body.BodyData.FormData.Data[i]
 | |
| 			formDataVars = append(formDataVars, p.Name)
 | |
| 			if p.Input.Type == vo.VariableTypeString && p.Input.AssistType > vo.AssistTypeNotSet && p.Input.AssistType < vo.AssistTypeTime {
 | |
| 				bodyConfig.FormDataConfig.FileTypeMapping[p.Name] = true
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		md5FieldMapping.SetBodyFields(formDataVars...)
 | |
| 	case BodyTypeRawText:
 | |
| 		TextTpl := inputs.Body.BodyData.RawText
 | |
| 		bodyConfig.TextPlainConfig = &TextPlainConfig{
 | |
| 			Tpl: TextTpl,
 | |
| 		}
 | |
| 		textPlainVars := extractBracesContent(TextTpl)
 | |
| 		md5FieldMapping.SetBodyFields(textPlainVars...)
 | |
| 	case BodyTypeFormURLEncoded:
 | |
| 		formURLEncodedVars := make([]string, 0)
 | |
| 		for _, p := range inputs.Body.BodyData.FormURLEncoded {
 | |
| 			formURLEncodedVars = append(formURLEncodedVars, p.Name)
 | |
| 		}
 | |
| 		md5FieldMapping.SetBodyFields(formURLEncodedVars...)
 | |
| 	}
 | |
| 	c.BodyConfig = bodyConfig
 | |
| 	c.MD5FieldMapping = *md5FieldMapping
 | |
| 
 | |
| 	if inputs.Setting != nil {
 | |
| 		c.Timeout = time.Duration(inputs.Setting.Timeout) * time.Second
 | |
| 		c.RetryTimes = uint64(inputs.Setting.RetryTimes)
 | |
| 	}
 | |
| 
 | |
| 	if err := setHttpRequesterInputsForNodeSchema(n, ns, implicitDeps); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if err := convert.SetOutputTypesForNodeSchema(n, ns); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return ns, nil
 | |
| }
 | |
| 
 | |
| func convertAuthType(auth string) (AuthType, error) {
 | |
| 	switch auth {
 | |
| 	case "CUSTOM_AUTH":
 | |
| 		return Custom, nil
 | |
| 	case "BEARER_AUTH":
 | |
| 		return BearToken, nil
 | |
| 	default:
 | |
| 		return AuthType(0), fmt.Errorf("invalid auth type")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func convertLocation(l string) (Location, error) {
 | |
| 	switch l {
 | |
| 	case "header":
 | |
| 		return Header, nil
 | |
| 	case "query":
 | |
| 		return QueryParam, nil
 | |
| 	default:
 | |
| 		return 0, fmt.Errorf("invalid location")
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| func (c *Config) Build(_ context.Context, _ *schema.NodeSchema, _ ...schema.BuildOption) (any, error) {
 | |
| 	if len(c.Method) == 0 {
 | |
| 		return nil, fmt.Errorf("method is requried")
 | |
| 	}
 | |
| 
 | |
| 	hg := &HTTPRequester{
 | |
| 		urlConfig:       c.URLConfig,
 | |
| 		method:          c.Method,
 | |
| 		retryTimes:      c.RetryTimes,
 | |
| 		authConfig:      c.AuthConfig,
 | |
| 		bodyConfig:      c.BodyConfig,
 | |
| 		md5FieldMapping: c.MD5FieldMapping,
 | |
| 	}
 | |
| 	client := http.DefaultClient
 | |
| 	if c.Timeout > 0 {
 | |
| 		client.Timeout = c.Timeout
 | |
| 	}
 | |
| 
 | |
| 	hg.client = client
 | |
| 
 | |
| 	return hg, nil
 | |
| }
 | |
| 
 | |
| type HTTPRequester struct {
 | |
| 	client          *http.Client
 | |
| 	urlConfig       URLConfig
 | |
| 	authConfig      *AuthenticationConfig
 | |
| 	bodyConfig      BodyConfig
 | |
| 	method          string
 | |
| 	retryTimes      uint64
 | |
| 	md5FieldMapping MD5FieldMapping
 | |
| }
 | |
| 
 | |
| func (hg *HTTPRequester) Invoke(ctx context.Context, input map[string]any) (output map[string]any, err error) {
 | |
| 	var (
 | |
| 		req         = &Request{}
 | |
| 		method      = hg.method
 | |
| 		retryTimes  = hg.retryTimes
 | |
| 		body        io.ReadCloser
 | |
| 		contentType string
 | |
| 		response    *http.Response
 | |
| 	)
 | |
| 
 | |
| 	req, err = hg.parserToRequest(input)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	httpRequest := &http.Request{
 | |
| 		Method: method,
 | |
| 		Header: http.Header{},
 | |
| 	}
 | |
| 
 | |
| 	httpURL, err := nodes.TemplateRender(hg.urlConfig.Tpl, req.URLVars)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	for key, value := range req.Headers {
 | |
| 		httpRequest.Header.Set(key, value)
 | |
| 	}
 | |
| 
 | |
| 	u, err := url.Parse(httpURL)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	params := u.Query()
 | |
| 	for key, value := range req.Params {
 | |
| 		params.Set(key, value)
 | |
| 	}
 | |
| 
 | |
| 	if hg.authConfig != nil {
 | |
| 		httpRequest.Header, params, err = hg.authConfig.addAuthentication(ctx, req.Authentication, httpRequest.Header, params)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 	u.RawQuery = params.Encode()
 | |
| 	httpRequest.URL = u
 | |
| 
 | |
| 	body, contentType, err = hg.bodyConfig.getBodyAndContentType(ctx, req)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if body != nil {
 | |
| 		httpRequest.Body = body
 | |
| 	}
 | |
| 
 | |
| 	if contentType != "" {
 | |
| 		httpRequest.Header.Add(HeaderContentType, contentType)
 | |
| 	}
 | |
| 
 | |
| 	for i := uint64(0); i <= retryTimes; i++ {
 | |
| 		response, err = hg.client.Do(httpRequest)
 | |
| 		if err == nil {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	result := make(map[string]any)
 | |
| 
 | |
| 	headers := func() string {
 | |
| 		// The structure of httpResp.Header is map[string][]string
 | |
| 		// If there are multiple header values, the last one will be selected by default
 | |
| 		hds := make(map[string]string, len(response.Header))
 | |
| 		for key, values := range response.Header {
 | |
| 			if len(values) == 0 {
 | |
| 				hds[key] = ""
 | |
| 			} else {
 | |
| 				hds[key] = values[len(values)-1]
 | |
| 			}
 | |
| 		}
 | |
| 		bs, _ := json.Marshal(hds)
 | |
| 		return string(bs)
 | |
| 	}()
 | |
| 	result["headers"] = headers
 | |
| 	var bodyBytes []byte
 | |
| 
 | |
| 	if response.Body != nil {
 | |
| 		defer func() {
 | |
| 			_ = response.Body.Close()
 | |
| 		}()
 | |
| 
 | |
| 		bodyBytes, err = io.ReadAll(response.Body)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if response.StatusCode >= http.StatusBadRequest {
 | |
| 		return nil, fmt.Errorf("request %v failed, response status code=%d, status=%v, headers=%v, body=%v",
 | |
| 			httpURL, response.StatusCode, response.Status, headers, string(bodyBytes))
 | |
| 	}
 | |
| 
 | |
| 	result["body"] = string(bodyBytes)
 | |
| 	result["statusCode"] = int64(response.StatusCode)
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // decodeUnicode parses the Unicode escape sequence in the string
 | |
| func decodeUnicode(s string) string {
 | |
| 	var result strings.Builder
 | |
| 	for i := 0; i < len(s); {
 | |
| 		if i+1 < len(s) && s[i] == '\\' && s[i+1] == 'u' {
 | |
| 			if i+6 <= len(s) {
 | |
| 				hexStr := s[i+2 : i+6]
 | |
| 				if code, err := strconv.ParseInt(hexStr, 16, 32); err == nil {
 | |
| 					result.WriteRune(rune(code))
 | |
| 					i += 6
 | |
| 					continue
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		result.WriteByte(s[i])
 | |
| 		i++
 | |
| 	}
 | |
| 	return result.String()
 | |
| }
 | |
| 
 | |
| func (authCfg *AuthenticationConfig) addAuthentication(_ context.Context, auth *Authentication, header http.Header, params url.Values) (
 | |
| 	http.Header, url.Values, error) {
 | |
| 
 | |
| 	if authCfg.Type == BearToken {
 | |
| 		header.Set(HeaderAuthorization, HeaderBearerPrefix+auth.Token)
 | |
| 		return header, params, nil
 | |
| 	}
 | |
| 	if authCfg.Type == Custom && authCfg.Location == Header {
 | |
| 		header.Set(auth.Key, auth.Value)
 | |
| 		return header, params, nil
 | |
| 	}
 | |
| 
 | |
| 	if authCfg.Type == Custom && authCfg.Location == QueryParam {
 | |
| 		params.Set(auth.Key, auth.Value)
 | |
| 		return header, params, nil
 | |
| 	}
 | |
| 
 | |
| 	return header, params, nil
 | |
| }
 | |
| 
 | |
| func (b *BodyConfig) getBodyAndContentType(ctx context.Context, req *Request) (io.ReadCloser, string, error) {
 | |
| 	var (
 | |
| 		body        io.Reader
 | |
| 		contentType string
 | |
| 	)
 | |
| 
 | |
| 	// body none return body nil
 | |
| 	if b.BodyType == BodyTypeNone {
 | |
| 		return nil, "", nil
 | |
| 	}
 | |
| 
 | |
| 	switch b.BodyType {
 | |
| 	case BodyTypeJSON:
 | |
| 		jsonString, err := nodes.TemplateRender(b.TextJsonConfig.Tpl, req.JsonVars)
 | |
| 		if err != nil {
 | |
| 			return nil, contentType, err
 | |
| 		}
 | |
| 		body = strings.NewReader(jsonString)
 | |
| 		contentType = ContentTypeJSON
 | |
| 	case BodyTypeFormURLEncoded:
 | |
| 		form := url.Values{}
 | |
| 		for key, value := range req.FormURLEncodedVars {
 | |
| 			form.Add(key, value)
 | |
| 		}
 | |
| 
 | |
| 		body = strings.NewReader(form.Encode())
 | |
| 		contentType = ContentTypeFormURLEncoded
 | |
| 	case BodyTypeRawText:
 | |
| 		textString, err := nodes.TemplateRender(b.TextPlainConfig.Tpl, req.TextPlainVars)
 | |
| 		if err != nil {
 | |
| 			return nil, contentType, err
 | |
| 		}
 | |
| 
 | |
| 		body = strings.NewReader(textString)
 | |
| 		contentType = ContentTypePlainText
 | |
| 	case BodyTypeBinary:
 | |
| 		if req.FileURL == nil {
 | |
| 			return nil, contentType, fmt.Errorf("file url is required")
 | |
| 		}
 | |
| 
 | |
| 		fileURL := *req.FileURL
 | |
| 		response, err := httpGet(ctx, fileURL)
 | |
| 		if err != nil {
 | |
| 			return nil, contentType, err
 | |
| 		}
 | |
| 
 | |
| 		body = response.Body
 | |
| 		contentType = ContentTypeBinary
 | |
| 	case BodyTypeFormData:
 | |
| 		var buffer = &bytes.Buffer{}
 | |
| 		formDataConfig := b.FormDataConfig
 | |
| 		writer := multipart.NewWriter(buffer)
 | |
| 
 | |
| 		total := int64(0)
 | |
| 		for key, value := range req.FormDataVars {
 | |
| 			if ok := formDataConfig.FileTypeMapping[key]; ok {
 | |
| 				fileWrite, err := writer.CreateFormFile(key, key)
 | |
| 				if err != nil {
 | |
| 					return nil, contentType, err
 | |
| 				}
 | |
| 
 | |
| 				response, err := httpGet(ctx, value)
 | |
| 				if err != nil {
 | |
| 					return nil, contentType, err
 | |
| 				}
 | |
| 
 | |
| 				if response.StatusCode != http.StatusOK {
 | |
| 					return nil, contentType, fmt.Errorf("failed to download file: %s, status code %v", value, response.StatusCode)
 | |
| 				}
 | |
| 
 | |
| 				size, err := io.Copy(fileWrite, response.Body)
 | |
| 				if err != nil {
 | |
| 					return nil, contentType, err
 | |
| 				}
 | |
| 
 | |
| 				total += size
 | |
| 				if total > maxSize {
 | |
| 					return nil, contentType, fmt.Errorf("too large body, total size: %d", total)
 | |
| 				}
 | |
| 			} else {
 | |
| 				err := writer.WriteField(key, value)
 | |
| 				if err != nil {
 | |
| 					return nil, contentType, err
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		_ = writer.Close()
 | |
| 		contentType = writer.FormDataContentType()
 | |
| 		body = buffer
 | |
| 	default:
 | |
| 		return nil, contentType, fmt.Errorf("unknown content type %s", b.BodyType)
 | |
| 	}
 | |
| 
 | |
| 	if _, ok := body.(io.ReadCloser); ok {
 | |
| 		return body.(io.ReadCloser), contentType, nil
 | |
| 	}
 | |
| 
 | |
| 	return io.NopCloser(body), contentType, nil
 | |
| }
 | |
| 
 | |
| func httpGet(ctx context.Context, url string) (*http.Response, error) {
 | |
| 	request, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	http.DefaultClient.Timeout = time.Second * defaultGetFileTimeout
 | |
| 	return http.DefaultClient.Do(request)
 | |
| }
 | |
| 
 | |
| func (hg *HTTPRequester) ToCallbackInput(_ context.Context, input map[string]any) (map[string]any, error) {
 | |
| 	var request = &Request{}
 | |
| 
 | |
| 	request, err := hg.parserToRequest(input)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	result := make(map[string]any)
 | |
| 	result["method"] = hg.method
 | |
| 
 | |
| 	u, err := nodes.TemplateRender(hg.urlConfig.Tpl, request.URLVars)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	result["url"] = u
 | |
| 
 | |
| 	params := make(map[string]any, len(request.Params))
 | |
| 	for k, v := range request.Params {
 | |
| 		params[k] = v
 | |
| 	}
 | |
| 	result["param"] = params
 | |
| 
 | |
| 	headers := make(map[string]any, len(request.Headers))
 | |
| 	for k, v := range request.Headers {
 | |
| 		headers[k] = v
 | |
| 	}
 | |
| 	result["header"] = headers
 | |
| 	result["auth"] = nil
 | |
| 	if hg.authConfig != nil {
 | |
| 		if hg.authConfig.Type == Custom {
 | |
| 			result["auth"] = map[string]interface{}{
 | |
| 				"Key":   request.Authentication.Key,
 | |
| 				"Value": request.Authentication.Value,
 | |
| 			}
 | |
| 		} else if hg.authConfig.Type == BearToken {
 | |
| 			result["auth"] = map[string]interface{}{
 | |
| 				"token": request.Authentication.Token,
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	result["body"] = nil
 | |
| 	switch hg.bodyConfig.BodyType {
 | |
| 	case BodyTypeJSON:
 | |
| 		js, err := nodes.TemplateRender(hg.bodyConfig.TextJsonConfig.Tpl, request.JsonVars)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		ret := make(map[string]any)
 | |
| 		err = sonic.Unmarshal([]byte(js), &ret)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		result["body"] = ret
 | |
| 	case BodyTypeRawText:
 | |
| 		tx, err := nodes.TemplateRender(hg.bodyConfig.TextPlainConfig.Tpl, request.TextPlainVars)
 | |
| 		if err != nil {
 | |
| 
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		result["body"] = tx
 | |
| 	case BodyTypeFormData:
 | |
| 		result["body"] = request.FormDataVars
 | |
| 	case BodyTypeFormURLEncoded:
 | |
| 		result["body"] = request.FormURLEncodedVars
 | |
| 	case BodyTypeBinary:
 | |
| 		result["body"] = request.FileURL
 | |
| 
 | |
| 	}
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	apiInfoURLPrefix = "__apiInfo_url_"
 | |
| 	headersPrefix    = "__headers_"
 | |
| 	paramsPrefix     = "__params_"
 | |
| 
 | |
| 	authDataPrefix            = "__auth_authData_"
 | |
| 	authBearerTokenDataPrefix = "bearerTokenData_token"
 | |
| 	authCustomDataPrefix      = "customData_data"
 | |
| 
 | |
| 	bodyDataPrefix           = "__body_bodyData_"
 | |
| 	bodyJsonPrefix           = "json_"
 | |
| 	bodyFormDataPrefix       = "formData_"
 | |
| 	bodyFormURLEncodedPrefix = "formURLEncoded_"
 | |
| 	bodyRawTextPrefix        = "rawText_"
 | |
| 	bodyBinaryFileURLPrefix  = "binary_fileURL"
 | |
| )
 | |
| 
 | |
| func (hg *HTTPRequester) parserToRequest(input map[string]any) (*Request, error) {
 | |
| 	request := &Request{
 | |
| 		URLVars:            make(map[string]any),
 | |
| 		Headers:            make(map[string]string),
 | |
| 		Params:             make(map[string]string),
 | |
| 		Authentication:     &Authentication{},
 | |
| 		FormURLEncodedVars: make(map[string]string),
 | |
| 		JsonVars:           make(map[string]any),
 | |
| 		TextPlainVars:      make(map[string]any),
 | |
| 		FormDataVars:       map[string]string{},
 | |
| 	}
 | |
| 	for key, value := range input {
 | |
| 		if strings.HasPrefix(key, apiInfoURLPrefix) {
 | |
| 			urlMD5 := strings.TrimPrefix(key, apiInfoURLPrefix)
 | |
| 			if urlKey, ok := hg.md5FieldMapping.URLMD5Mapping[urlMD5]; ok {
 | |
| 				if strings.HasPrefix(urlKey, "global_variable_") {
 | |
| 					urlKey = globalVariableReplaceRegexp.ReplaceAllString(urlKey, "global_variable_$1.$2")
 | |
| 				}
 | |
| 				nodes.SetMapValue(request.URLVars, strings.Split(urlKey, "."), value.(string))
 | |
| 			}
 | |
| 		}
 | |
| 		if strings.HasPrefix(key, headersPrefix) {
 | |
| 			headerKeyMD5 := strings.TrimPrefix(key, headersPrefix)
 | |
| 			if headerKey, ok := hg.md5FieldMapping.HeaderMD5Mapping[headerKeyMD5]; ok {
 | |
| 				request.Headers[headerKey] = value.(string)
 | |
| 			}
 | |
| 		}
 | |
| 		if strings.HasPrefix(key, paramsPrefix) {
 | |
| 			paramKeyMD5 := strings.TrimPrefix(key, paramsPrefix)
 | |
| 			if paramKey, ok := hg.md5FieldMapping.ParamMD5Mapping[paramKeyMD5]; ok {
 | |
| 				request.Params[paramKey] = value.(string)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if strings.HasPrefix(key, authDataPrefix) {
 | |
| 			authKey := strings.TrimPrefix(key, authDataPrefix)
 | |
| 			if strings.HasPrefix(authKey, authBearerTokenDataPrefix) {
 | |
| 				request.Authentication.Token = value.(string) // bear
 | |
| 			}
 | |
| 			if strings.HasPrefix(authKey, authCustomDataPrefix) {
 | |
| 				if key == "__auth_authData_customData_data_Key" {
 | |
| 					request.Authentication.Key = value.(string)
 | |
| 				}
 | |
| 				if key == "__auth_authData_customData_data_Value" {
 | |
| 					request.Authentication.Value = value.(string)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if strings.HasPrefix(key, bodyDataPrefix) {
 | |
| 			bodyKey := strings.TrimPrefix(key, bodyDataPrefix)
 | |
| 			if strings.HasPrefix(bodyKey, bodyJsonPrefix) {
 | |
| 				jsonMd5Key := strings.TrimPrefix(bodyKey, bodyJsonPrefix)
 | |
| 				if jsonKey, ok := hg.md5FieldMapping.BodyMD5Mapping[jsonMd5Key]; ok {
 | |
| 					if strings.HasPrefix(jsonKey, "global_variable_") {
 | |
| 						jsonKey = globalVariableReplaceRegexp.ReplaceAllString(jsonKey, "global_variable_$1.$2")
 | |
| 					}
 | |
| 					nodes.SetMapValue(request.JsonVars, strings.Split(jsonKey, "."), value)
 | |
| 				}
 | |
| 
 | |
| 			}
 | |
| 			if strings.HasPrefix(bodyKey, bodyFormDataPrefix) {
 | |
| 				formDataMd5Key := strings.TrimPrefix(bodyKey, bodyFormDataPrefix)
 | |
| 				if formDataKey, ok := hg.md5FieldMapping.BodyMD5Mapping[formDataMd5Key]; ok {
 | |
| 					request.FormDataVars[formDataKey] = value.(string)
 | |
| 				}
 | |
| 
 | |
| 			}
 | |
| 
 | |
| 			if strings.HasPrefix(bodyKey, bodyFormURLEncodedPrefix) {
 | |
| 				formURLEncodeMd5Key := strings.TrimPrefix(bodyKey, bodyFormURLEncodedPrefix)
 | |
| 				if formURLEncodeKey, ok := hg.md5FieldMapping.BodyMD5Mapping[formURLEncodeMd5Key]; ok {
 | |
| 					request.FormURLEncodedVars[formURLEncodeKey] = value.(string)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if strings.HasPrefix(bodyKey, bodyRawTextPrefix) {
 | |
| 				rawTextMd5Key := strings.TrimPrefix(bodyKey, bodyRawTextPrefix)
 | |
| 				if rawTextKey, ok := hg.md5FieldMapping.BodyMD5Mapping[rawTextMd5Key]; ok {
 | |
| 					if strings.HasPrefix(rawTextKey, "global_variable_") {
 | |
| 						rawTextKey = globalVariableReplaceRegexp.ReplaceAllString(rawTextKey, "global_variable_$1.$2")
 | |
| 					}
 | |
| 					nodes.SetMapValue(request.TextPlainVars, strings.Split(rawTextKey, "."), value)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if strings.HasPrefix(bodyKey, bodyBinaryFileURLPrefix) {
 | |
| 				request.FileURL = ptr.Of(value.(string))
 | |
| 			}
 | |
| 
 | |
| 		}
 | |
| 
 | |
| 	}
 | |
| 
 | |
| 	return request, nil
 | |
| }
 | |
| 
 | |
| func (hg *HTTPRequester) ToCallbackOutput(_ context.Context, out map[string]any) (*nodes.StructuredCallbackOutput, error) {
 | |
| 	if body, ok := out["body"]; !ok {
 | |
| 		return &nodes.StructuredCallbackOutput{
 | |
| 			RawOutput: out,
 | |
| 			Output:    out,
 | |
| 		}, nil
 | |
| 	} else {
 | |
| 		output := maps.Clone(out)
 | |
| 		output["body"] = decodeUnicode(body.(string))
 | |
| 		return &nodes.StructuredCallbackOutput{
 | |
| 			RawOutput: out,
 | |
| 			Output:    output,
 | |
| 		}, nil
 | |
| 	}
 | |
| 
 | |
| }
 |