first commit
This commit is contained in:
166
core/network/multipart.go
Normal file
166
core/network/multipart.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
// ParseMultipartFormFields extracts text form fields from a multipart/form-data body,
|
||||
// skipping file parts to avoid loading binary data into memory.
|
||||
func ParseMultipartFormFields(contentType string, body []byte) (map[string]any, error) {
|
||||
_, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boundary := params["boundary"]
|
||||
if boundary == "" {
|
||||
return nil, fmt.Errorf("no boundary in content-type")
|
||||
}
|
||||
reader := multipart.NewReader(bytes.NewReader(body), boundary)
|
||||
payload := make(map[string]any)
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if part.FileName() != "" {
|
||||
_ = part.Close()
|
||||
continue
|
||||
}
|
||||
name := part.FormName()
|
||||
if name != "" {
|
||||
val, readErr := io.ReadAll(part)
|
||||
if readErr != nil {
|
||||
_ = part.Close()
|
||||
return nil, readErr
|
||||
}
|
||||
payload[name] = string(val)
|
||||
}
|
||||
_ = part.Close()
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// ReconstructMultipartBody rebuilds a multipart/form-data body from the original,
|
||||
// replacing text field values with those from payload (e.g. updated "model") and
|
||||
// copying file parts byte-for-byte.
|
||||
func ReconstructMultipartBody(origContentType string, origBody []byte, payload map[string]any) ([]byte, string, error) {
|
||||
_, params, err := mime.ParseMediaType(origContentType)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
boundary := params["boundary"]
|
||||
if boundary == "" {
|
||||
return nil, "", fmt.Errorf("no boundary in content-type")
|
||||
}
|
||||
reader := multipart.NewReader(bytes.NewReader(origBody), boundary)
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
writtenFields := make(map[string]bool)
|
||||
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
name := part.FormName()
|
||||
if part.FileName() != "" {
|
||||
fw, createErr := writer.CreatePart(part.Header)
|
||||
if createErr != nil {
|
||||
_ = part.Close()
|
||||
return nil, "", createErr
|
||||
}
|
||||
if _, copyErr := io.Copy(fw, part); copyErr != nil {
|
||||
_ = part.Close()
|
||||
return nil, "", copyErr
|
||||
}
|
||||
} else if name != "" {
|
||||
if val, ok := payload[name]; ok {
|
||||
if err := WriteMultipartField(writer, name, val); err != nil {
|
||||
_ = part.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
} else {
|
||||
origVal, readErr := io.ReadAll(part)
|
||||
if readErr != nil {
|
||||
_ = part.Close()
|
||||
return nil, "", readErr
|
||||
}
|
||||
if err := writer.WriteField(name, string(origVal)); err != nil {
|
||||
_ = part.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
writtenFields[name] = true
|
||||
}
|
||||
_ = part.Close()
|
||||
}
|
||||
|
||||
for key, val := range payload {
|
||||
if writtenFields[key] {
|
||||
continue
|
||||
}
|
||||
if err := WriteMultipartField(writer, key, val); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return buf.Bytes(), writer.FormDataContentType(), nil
|
||||
}
|
||||
|
||||
// WriteMultipartField writes a single form field to the multipart writer,
|
||||
// handling string, []string, and other value types.
|
||||
func WriteMultipartField(writer *multipart.Writer, name string, val any) error {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
return writer.WriteField(name, v)
|
||||
case []string:
|
||||
encoded, err := schemas.MarshalSorted(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writer.WriteField(name, string(encoded))
|
||||
default:
|
||||
return writer.WriteField(name, fmt.Sprintf("%v", val))
|
||||
}
|
||||
}
|
||||
|
||||
// SerializePayloadToRequest writes the modified payload back to req.Body,
|
||||
// using multipart reconstruction for multipart/form-data or JSON for everything else.
|
||||
func SerializePayloadToRequest(req *schemas.HTTPRequest, payload map[string]any, isMultipart bool, origContentType string) error {
|
||||
if isMultipart {
|
||||
newBody, newCT, err := ReconstructMultipartBody(origContentType, req.Body, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Body = newBody
|
||||
for k := range req.Headers {
|
||||
if strings.EqualFold(k, "content-type") {
|
||||
delete(req.Headers, k)
|
||||
}
|
||||
}
|
||||
req.Headers["Content-Type"] = newCT
|
||||
return nil
|
||||
}
|
||||
body, err := schemas.MarshalSorted(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Body = body
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user