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

  1. Change the endpoint's Request type parameter to a struct whose fields carry the api form-field tags.
  2. If the raw form should be passed through, use MultipartFields as the request type, which DecodeMultipart special-cases.
  3. 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

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)