{"record":{"id":"c85979fce526aa22","repo":"siyuan-note/siyuan","slug":"multipart-request-must-be-a-struct","errorCode":null,"errorMessage":"multipart request must be a struct","messagePattern":"multipart request must be a struct","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/multipart.go","lineNumber":59,"sourceCode":"\t}\n\tif form == nil {\n\t\treturn request, fmt.Errorf(\"multipart form is missing\")\n\t}\n\tvalue := reflect.ValueOf(&request).Elem()\n\tif value.Type() == reflect.TypeFor[ExtensionCopyRequest]() {\n\t\tdecoded, decodeErr := decodeExtensionCopyForm(form)\n\t\tif decodeErr != nil {\n\t\t\treturn request, decodeErr\n\t\t}\n\t\tvalue.Set(reflect.ValueOf(decoded))\n\t\treturn\n\t}\n\tif value.Type() == reflect.TypeFor[MultipartFields]() {\n\t\tvalue.Set(reflect.ValueOf(MultipartFields{Value: form.Value, File: form.File}))\n\t\treturn\n\t}\n\tif value.Kind() != reflect.Struct {\n\t\treturn request, fmt.Errorf(\"multipart request must be a struct\")\n\t}\n\tif err = validateMultipartRequest(value.Type()); err != nil {\n\t\treturn\n\t}\n\tfor i := 0; i < value.NumField(); i++ {\n\t\tfield := value.Type().Field(i)\n\t\tname := strings.Split(field.Tag.Get(\"json\"), \",\")[0]\n\t\toptional := strings.Contains(\",\"+field.Tag.Get(\"api\")+\",\", \",optional,\")\n\t\tswitch field.Type {\n\t\tcase reflect.TypeFor[*multipart.FileHeader](), reflect.TypeFor[[]*multipart.FileHeader]():\n\t\t\tfiles := form.File[name]\n\t\t\tif len(files) == 0 {\n\t\t\t\tif !optional {\n\t\t\t\t\treturn request, fmt.Errorf(\"%s not found\", name)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif field.Type.Kind() == reflect.Slice {","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/multipart.go#L41-L77","documentation":"After decoding, DecodeMultipart reflects over the request type to map form fields onto struct fields (unless it is the special MultipartFields catch-all). If the generic Request type parameter is not a struct, reflection cannot map fields and the endpoint is misconfigured. This is a server-side contract/type error, not a client input error.","triggerScenarios":"An endpoint's Request type parameter is a non-struct type (e.g. map[string]string, a primitive, or an alias) while its body kind is multipart/form.","commonSituations":"Developer declares Endpoint[Request, Data] with Request as a scalar or map by mistake when wiring a new multipart endpoint; refactoring changed the request type away from a struct.","solutions":["Change the endpoint's Request type parameter to a struct whose fields carry the api form-field tags.","If the raw form should be passed through, use MultipartFields as the request type, which DecodeMultipart special-cases.","Add a unit test decoding a sample form so misconfigured request types fail at test time."],"exampleFix":"// before\ntype req = map[string]string\nEndpoint[req, Data]{...}\n// after\ntype req struct {\n  Name string `api:\"name\"`\n  File *multipart.FileHeader `api:\"file\"`\n}\nEndpoint[req, Data]{...}","handlingStrategy":"type-guard","validationCode":"// compile-time: var _ = func(r Request) { /* ensure struct */ } ; runtime not applicable\nfunc assertStruct[Req any]() { var r Req; _ = reflect.ValueOf(r).Kind() == reflect.Struct }","typeGuard":"func isStruct[Req any]() bool { t := reflect.TypeOf((*Req)(nil)).Elem(); return t.Kind() == reflect.Struct }","tryCatchPattern":"if err := decode(req); err != nil && strings.Contains(err.Error(), \"must be a struct\") { log.Fatalf(\"endpoint request type misconfigured: %v\", err) }","preventionTips":["Always declare multipart endpoint Request types as tagged structs or MultipartFields.","Add contract tests that decode a sample form for every multipart endpoint.","Review Request type parameters after refactors."],"tags":["go","reflection","multipart"],"backgroundTag":"type-mismatch","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}