mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
## What does this PR do? Fixes #4156 When posting form data with file upload, if a field value contains `=` or `&`, the value was being truncated. ### Example ```go data := g.Map{ "file": "@file:/path/to/file.txt", "fieldName": "aaa=1&b=2", } client.Post(ctx, "/upload", data) ``` **Expected**: Server receives `fieldName = "aaa=1&b=2"` **Actual (before fix)**: Server receives `fieldName = "aaa"` (truncated) ## Root Cause Analysis The issue was caused by three problems in the original code: ### Problem 1: Global URL encoding disable (httputils.go) ```go // Original code - PROBLEMATIC if urlEncode { for k, v := range m { if gstr.Contains(k, fileUploadingKey) || gstr.Contains(gconv.String(v), fileUploadingKey) { urlEncode = false // Disables URL encoding for ALL values! break } } } ``` When any value contained `@file:`, URL encoding was disabled for ALL values, causing `"aaa=1&b=2"` to remain unencoded. The `&` character was then treated as a parameter separator. ### Problem 2: Split on all `=` characters (gclient_request.go) ```go // Original code - PROBLEMATIC array := strings.Split(item, "=") // Splits on ALL '=' characters ``` This caused `"fieldName=aaa=1"` to be split into `["fieldName", "aaa", "1"]`. ### Problem 3: No URL decoding for field values URL-encoded values were written directly to the multipart form without decoding. ## Solution ### Fix 1: Remove global URL encoding disable Only `@file:` prefixed values are kept unencoded for file upload detection. Other values are properly URL-encoded. ### Fix 2: Use SplitN to limit split count ```go array := strings.SplitN(item, "=", 2) // Only split on first '=' ``` ### Fix 3: Add URL decoding for field values ```go if v, err := gurl.Decode(fieldValue); err == nil { fieldValue = v } ``` ## Compatibility Analysis | Scenario | Before | After | Compatible | |----------|--------|-------|------------| | Normal form POST (no file upload) | ✅ Works | ✅ Works | ✅ Yes | | File upload + normal field values | ✅ Works | ✅ Works | ✅ Yes | | File upload + field values containing `=` or `&` | ❌ Truncated | ✅ Works | ✅ Fixed | | Field value is `@file:` (no path) | ✅ Works | ✅ Works | ✅ Yes | | Field value starts with `@file:` but file doesn't exist | ❌ Error | ❌ Error | ✅ Yes | | User sends pre-encoded value like `"aaa%3D1"` | ✅ Works | ✅ Works | ✅ Yes | | Content-Type: application/json | ✅ Works | ✅ Works | ✅ Yes | | Content-Type: application/xml | ✅ Works | ✅ Works | ✅ Yes | ### Breaking Change Assessment **No breaking changes.** The fix only affects the file upload scenario where field values contain special characters (`=`, `&`). Previously this scenario was broken, now it works correctly. ### Edge Cases 1. **Literal `@file:` value**: GoFrame treats `@file:` as a special marker for file upload. This is a framework design decision and remains unchanged. 2. **URL decode failure**: If URL decoding fails (e.g., invalid `%XX` sequence), the original value is preserved. ## Test Coverage Added comprehensive tests covering: - `Test_Issue4156` - Basic fix verification - `Test_Issue4156_MultipleSpecialChars` - Multiple `=`, `&`, `%`, `+`, spaces - `Test_Issue4156_MultipleFields` - Multiple fields with special characters - `Test_Issue4156_NoFileUpload` - Normal POST without file upload - `Test_Issue4156_PreEncodedValue` - Pre-encoded values like `%3D` - `Test_Issue4156_EmptyAndSpecialValues` - Edge cases (`=` at start/end, only special chars) - `TestBuildParams_*` - httputil.BuildParams comprehensive tests All tests pass, including existing `Test_Issue3748` which tests the `@file:` marker handling. ## Files Changed - `internal/httputil/httputils.go` - Remove global URL encoding disable, adjust `@file:` condition - `internal/httputil/httputils_test.go` - Add comprehensive BuildParams tests - `net/gclient/gclient_request.go` - Use SplitN, add URL decoding - `net/gclient/gclient_z_unit_issue_test.go` - Add Issue 4156 test cases
83 lines
1.9 KiB
Go
83 lines
1.9 KiB
Go
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
|
//
|
|
// This Source Code Form is subject to the terms of the MIT License.
|
|
// If a copy of the MIT was not distributed with this file,
|
|
// You can obtain one at https://github.com/gogf/gf.
|
|
|
|
// Package httputil provides HTTP functions for internal usage only.
|
|
package httputil
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gogf/gf/v2/encoding/gurl"
|
|
"github.com/gogf/gf/v2/internal/empty"
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|
)
|
|
|
|
const (
|
|
fileUploadingKey = "@file:"
|
|
)
|
|
|
|
// BuildParams builds the request string for the http client. The `params` can be type of:
|
|
// string/[]byte/map/struct/*struct.
|
|
//
|
|
// The optional parameter `noUrlEncode` specifies whether ignore the url encoding for the data.
|
|
func BuildParams(params any, noUrlEncode ...bool) (encodedParamStr string) {
|
|
// If given string/[]byte, converts and returns it directly as string.
|
|
switch v := params.(type) {
|
|
case string, []byte:
|
|
return gconv.String(params)
|
|
case []any:
|
|
if len(v) > 0 {
|
|
params = v[0]
|
|
} else {
|
|
params = nil
|
|
}
|
|
}
|
|
// Else converts it to map and does the url encoding.
|
|
m, urlEncode := gconv.Map(params, gconv.MapOption{
|
|
OmitEmpty: true,
|
|
}), true
|
|
if len(m) == 0 {
|
|
return gconv.String(params)
|
|
}
|
|
if len(noUrlEncode) == 1 {
|
|
urlEncode = !noUrlEncode[0]
|
|
}
|
|
s := ""
|
|
for k, v := range m {
|
|
// Ignore nil attributes.
|
|
if empty.IsNil(v) {
|
|
continue
|
|
}
|
|
if len(encodedParamStr) > 0 {
|
|
encodedParamStr += "&"
|
|
}
|
|
s = gconv.String(v)
|
|
if urlEncode {
|
|
if strings.HasPrefix(s, fileUploadingKey) {
|
|
// No url encoding if value starts with file uploading marker.
|
|
} else {
|
|
s = gurl.Encode(s)
|
|
}
|
|
}
|
|
encodedParamStr += k + "=" + s
|
|
}
|
|
return
|
|
}
|
|
|
|
// HeaderToMap coverts request headers to map.
|
|
func HeaderToMap(header http.Header) map[string]any {
|
|
m := make(map[string]any)
|
|
for k, v := range header {
|
|
if len(v) > 1 {
|
|
m[k] = v
|
|
} else if len(v) == 1 {
|
|
m[k] = v[0]
|
|
}
|
|
}
|
|
return m
|
|
}
|