siyuan-note/siyuan · error
multipart request must be a struct
Error message
multipart request must be a struct
What it means
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.
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.
Example fix
// before
type req = map[string]string
Endpoint[req, Data]{...}
// after
type req struct {
Name string `api:"name"`
File *multipart.FileHeader `api:"file"`
}
Endpoint[req, Data]{...} Defensive patterns
Strategy: type-guard
Validate before calling
// compile-time: var _ = func(r Request) { /* ensure struct */ } ; runtime not applicable
func assertStruct[Req any]() { var r Req; _ = reflect.ValueOf(r).Kind() == reflect.Struct } Type guard
func isStruct[Req any]() bool { t := reflect.TypeOf((*Req)(nil)).Elem(); return t.Kind() == reflect.Struct } Try / catch
if err := decode(req); err != nil && strings.Contains(err.Error(), "must be a struct") { log.Fatalf("endpoint request type misconfigured: %v", err) } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/c85979fce526aa22.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/multipart.go:59
}
if form == nil {
return request, fmt.Errorf("multipart form is missing")
}
value := reflect.ValueOf(&request).Elem()
if value.Type() == reflect.TypeFor[ExtensionCopyRequest]() {
decoded, decodeErr := decodeExtensionCopyForm(form)
if decodeErr != nil {
return request, decodeErr
}
value.Set(reflect.ValueOf(decoded))
return
}
if value.Type() == reflect.TypeFor[MultipartFields]() {
value.Set(reflect.ValueOf(MultipartFields{Value: form.Value, File: form.File}))
return
}
if value.Kind() != reflect.Struct {
return request, fmt.Errorf("multipart request must be a struct")
}
if err = validateMultipartRequest(value.Type()); err != nil {
return
}
for i := 0; i < value.NumField(); i++ {
field := value.Type().Field(i)
name := strings.Split(field.Tag.Get("json"), ",")[0]
optional := strings.Contains(","+field.Tag.Get("api")+",", ",optional,")
switch field.Type {
case reflect.TypeFor[*multipart.FileHeader](), reflect.TypeFor[[]*multipart.FileHeader]():
files := form.File[name]
if len(files) == 0 {
if !optional {
return request, fmt.Errorf("%s not found", name)
}
continue
}
if field.Type.Kind() == reflect.Slice {View on GitHub (pinned to 9f775e8a12)