fix(task-plugin): set multipart content type for upstream requests (#7655)

* fix(task-plugin): set multipart content type for upstream requests

* refactor(task-plugin): copy the rebuilt multipart Content-Type as is

BuildRequestBody stores writer.FormDataContentType() on the client
request right before BuildRequestHeader runs, so the media type and
boundary checks could never fail. Copy the value directly, as the sora
adaptor did before task plugins.

The test plugin now forwards the client's Content-Type, whose boundary
differs from the rebuilt body, so the test also fails if a plugin header
wins over the host value.

---------

Co-authored-by: CaIon <[email protected]>
This commit is contained in:
指短琴长
2026-10-08 23:52:03 +08:00
committed by GitHub
co-authored by CaIon
parent 6370b29424
commit 7aa3531ef4
2 changed files with 16 additions and 3 deletions
+6 -1
View File
@@ -262,13 +262,18 @@ func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, erro
return a.submit.URL, pluginruntime.ValidateRequestURL(a.submit.URL, info.ChannelBaseUrl, a.plugin.Meta.AllowedHosts)
}
func (a *TaskAdaptor) BuildRequestHeader(_ *gin.Context, req *http.Request, _ *relaycommon.RelayInfo) error {
func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, _ *relaycommon.RelayInfo) error {
if a.submit == nil {
return fmt.Errorf("plugin submit request was not built")
}
for name, value := range a.submit.Headers {
req.Header.Set(name, value)
}
// BuildRequestBody rebuilt the multipart body with a new boundary and put its
// Content-Type on the client request; a plugin's own Content-Type cannot match it.
if a.submit.BodyType == "multipart" {
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
}
return nil
}
+10 -2
View File
@@ -8,6 +8,7 @@ import (
"errors"
"io"
"math"
"mime"
"mime/multipart"
"net/http"
"net/http/httptest"
@@ -83,9 +84,10 @@ func TestTaskAdaptorRejectsDeprecatedClientResponse(t *testing.T) {
}
func TestTaskAdaptorBuildsMultipartFromOpaqueFileReference(t *testing.T) {
// The plugin forwards the client's Content-Type, whose boundary is not the one of the rebuilt body.
source := `
export const meta = {apiVersion:1,key:"multipart",name:"Multipart",version:"1.0.0",author:{name:"Test"},models:["m"],fetchMode:"per_task"};
export function buildSubmitRequest(ctx) { return {url:ctx.baseUrl+"/submit",bodyType:"multipart",parts:[{name:"model",value:"m"},{name:"input_reference",fileRef:ctx.files[0].ref}]}; }
export function buildSubmitRequest(ctx) { return {url:ctx.baseUrl+"/submit",headers:{"Content-Type":ctx.requestHeaders["Content-Type"]},bodyType:"multipart",parts:[{name:"model",value:"m"},{name:"input_reference",fileRef:ctx.files[0].ref}]}; }
export function parseSubmitResponse(ctx,r){return {taskId:"1"}} export function buildQueryRequest(){return {url:"https://example.com"}} export function parseTaskResult(){return {status:"SUCCESS"}}
`
plugin, err := pluginruntime.NewRegistry().Register(source, pluginruntime.Options{})
@@ -108,7 +110,13 @@ export function parseSubmitResponse(ctx,r){return {taskId:"1"}} export function
require.NoError(t, err)
requestBytes, err := io.ReadAll(body)
require.NoError(t, err)
reader := multipart.NewReader(bytes.NewReader(requestBytes), strings.TrimPrefix(c.GetHeader("Content-Type"), "multipart/form-data; boundary="))
upstreamRequest := httptest.NewRequest(http.MethodPost, "https://provider.example/submit", bytes.NewReader(requestBytes))
require.NoError(t, adaptor.BuildRequestHeader(c, upstreamRequest, info))
mediaType, params, err := mime.ParseMediaType(upstreamRequest.Header.Get("Content-Type"))
require.NoError(t, err)
assert.Equal(t, "multipart/form-data", mediaType)
require.NotEmpty(t, params["boundary"])
reader := multipart.NewReader(bytes.NewReader(requestBytes), params["boundary"])
form, err := reader.ReadForm(1024)
require.NoError(t, err)
assert.Equal(t, []string{"m"}, form.Value["model"])