Skip to content

Panic in addFormFieldInputFileUpload when InputFileUpload.Data holds a struct value (e.g. multipart.File from FormFile) #274

Description

@snvv133

Summary

addFormFieldInputFileUpload in build_request_form.go panics with reflect: call of reflect.Value.IsNil on struct Value when InputFileUpload.Data is an io.Reader whose underlying concrete type is a struct value (not a pointer/chan/map/slice/func/interface).

This is reachable in real code: multipart.File returned by (*multipart.FileHeader).Open() for in-memory parts is a sectionReadCloser struct value (not *sectionReadCloser). Forwarding it directly as InputFileUpload.Data crashes the entire process.

Versions

  • github.com/go-telegram/bot v1.19.0
  • Go 1.22+ (any version, this is a long-standing issue)

Reproduction

Minimal HTTP handler that uploads a small (< 32MB, in-memory) image to SendPhoto:

func upload(b *bot.Bot, w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(10 << 20)
    file, header, _ := r.FormFile(\"image\")
    defer file.Close()

    // PANIC here
    b.SendPhoto(r.Context(), &bot.SendPhotoParams{
        ChatID: chatID,
        Photo:  &models.InputFileUpload{Filename: header.Filename, Data: file},
    })
}

For files above the multipart in-memory threshold (default 32MB), multipart.FileHeader.Open() returns *os.File (a pointer) and the panic does not occur — which is why this bug only shows up for small files.

Actual panic

panic: reflect: call of reflect.Value.IsNil on struct Value

goroutine N [running]:
reflect.Value.IsNil(...)
github.com/go-telegram/bot.addFormFieldInputFileUpload(...)
github.com/go-telegram/bot.buildRequestForm(...)
github.com/go-telegram/bot.(*Bot).rawRequest.func1(...)

The panic happens inside the goroutine spawned by rawRequest, which has no recover, so the whole process exits.

Root cause

build_request_form.go:103:

func addFormFieldInputFileUpload(form *multipart.Writer, fieldName string, value *models.InputFileUpload) error {
    if value.Data == nil || reflect.ValueOf(value.Data).IsNil() {
        return fmt.Errorf(\"nil data for field %s\", fieldName)
    }
    ...
}

reflect.Value.IsNil() only accepts kinds Chan, Func, Map, Pointer, UnsafePointer, Interface, Slice. For any other kind it panics. value.Data is io.Reader, whose concrete type can legitimately be a struct value (as sectionReadCloser is).

Suggested fix

Guard the kind before calling IsNil():

func addFormFieldInputFileUpload(form *multipart.Writer, fieldName string, value *models.InputFileUpload) error {
    if value.Data == nil {
        return fmt.Errorf(\"nil data for field %s\", fieldName)
    }
    v := reflect.ValueOf(value.Data)
    switch v.Kind() {
    case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
        if v.IsNil() {
            return fmt.Errorf(\"nil data for field %s\", fieldName)
        }
    }
    // struct/array/string/number kinds: not nilable, fall through
    ...
}

rawRequest's background goroutine should also wrap the body builder in defer recover() so a future panic of any kind doesn't take down the host process — but the kind guard fixes the immediate bug.

Workaround

Wrap the reader in io.LimitReader (or any helper that returns a pointer) before passing it as InputFileUpload.Data:

Photo: &models.InputFileUpload{
    Filename: header.Filename,
    Data:     io.LimitReader(file, header.Size),  // *io.LimitedReader, IsNil-safe
}

This is what we ended up shipping after hitting the panic in production with multi-MB user-uploaded images.

Impact

Any project that reads a small file from multipart.FileHeader.Open() (the canonical Go pattern for handling browser uploads) and forwards it to Send{Photo,Document,Audio,...} will crash on every upload. The crash only shows up for files under ~32 MB, so it can hide in development with large test files and only fire in production with smaller user uploads — that's how we found it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions