From 7aa3531ef4c247ad4891c06cc8ed9d0ffb73fecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=8C=87=E7=9F=AD=E7=90=B4=E9=95=BF?= <89786902+LauZzL@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:52:03 +0800 Subject: [PATCH] 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 --- relay/channel/task/jsplugin/adaptor.go | 7 ++++++- relay/channel/task/jsplugin/adaptor_test.go | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/relay/channel/task/jsplugin/adaptor.go b/relay/channel/task/jsplugin/adaptor.go index ae17ccc5eb..815a2dc28e 100644 --- a/relay/channel/task/jsplugin/adaptor.go +++ b/relay/channel/task/jsplugin/adaptor.go @@ -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 } diff --git a/relay/channel/task/jsplugin/adaptor_test.go b/relay/channel/task/jsplugin/adaptor_test.go index b60fec4449..0f1d1b4904 100644 --- a/relay/channel/task/jsplugin/adaptor_test.go +++ b/relay/channel/task/jsplugin/adaptor_test.go @@ -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"])