POST data using the Content-Type multipart/form-data

121,852

Solution 1

Here's some sample code.

In short, you'll need to use the mime/multipart package to build the form.

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/httptest"
    "net/http/httputil"
    "os"
    "strings"
)

func main() {

    var client *http.Client
    var remoteURL string
    {
        //setup a mocked http client.
        ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            b, err := httputil.DumpRequest(r, true)
            if err != nil {
                panic(err)
            }
            fmt.Printf("%s", b)
        }))
        defer ts.Close()
        client = ts.Client()
        remoteURL = ts.URL
    }

    //prepare the reader instances to encode
    values := map[string]io.Reader{
        "file":  mustOpen("main.go"), // lets assume its this file
        "other": strings.NewReader("hello world!"),
    }
    err := Upload(client, remoteURL, values)
    if err != nil {
        panic(err)
    }
}

func Upload(client *http.Client, url string, values map[string]io.Reader) (err error) {
    // Prepare a form that you will submit to that URL.
    var b bytes.Buffer
    w := multipart.NewWriter(&b)
    for key, r := range values {
        var fw io.Writer
        if x, ok := r.(io.Closer); ok {
            defer x.Close()
        }
        // Add an image file
        if x, ok := r.(*os.File); ok {
            if fw, err = w.CreateFormFile(key, x.Name()); err != nil {
                return
            }
        } else {
            // Add other fields
            if fw, err = w.CreateFormField(key); err != nil {
                return
            }
        }
        if _, err = io.Copy(fw, r); err != nil {
            return err
        }

    }
    // Don't forget to close the multipart writer.
    // If you don't close it, your request will be missing the terminating boundary.
    w.Close()

    // Now that you have a form, you can submit it to your handler.
    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return
    }
    // Don't forget to set the content type, this will contain the boundary.
    req.Header.Set("Content-Type", w.FormDataContentType())

    // Submit the request
    res, err := client.Do(req)
    if err != nil {
        return
    }

    // Check the response
    if res.StatusCode != http.StatusOK {
        err = fmt.Errorf("bad status: %s", res.Status)
    }
    return
}

func mustOpen(f string) *os.File {
    r, err := os.Open(f)
    if err != nil {
        panic(err)
    }
    return r
}

Solution 2

Here's a function I've used that uses io.Pipe() to avoid reading in the entire file to memory or needing to manage any buffers. It handles only a single file, but could easily be extended to handle more by adding more parts within the goroutine. The happy path works well. The error paths have not hand much testing.

import (
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func UploadMultipartFile(client *http.Client, uri, key, path string) (*http.Response, error) {
    body, writer := io.Pipe()

    req, err := http.NewRequest(http.MethodPost, uri, body)
    if err != nil {
        return nil, err
    }

    mwriter := multipart.NewWriter(writer)
    req.Header.Add("Content-Type", mwriter.FormDataContentType())

    errchan := make(chan error)

    go func() {
        defer close(errchan)
        defer writer.Close()
        defer mwriter.Close()

        w, err := mwriter.CreateFormFile(key, path)
        if err != nil {
            errchan <- err
            return
        }

        in, err := os.Open(path)
        if err != nil {
            errchan <- err
            return
        }
        defer in.Close()

        if written, err := io.Copy(w, in); err != nil {
            errchan <- fmt.Errorf("error copying %s (%d bytes written): %v", path, written, err)
            return
        }

        if err := mwriter.Close(); err != nil {
            errchan <- err
            return
        }
    }()

    resp, err := client.Do(req)
    merr := <-errchan

    if err != nil || merr != nil {
        return resp, fmt.Errorf("http error: %v, multipart error: %v", err, merr)
    }

    return resp, nil
}

Solution 3

After having to decode the accepted answer for this question for use in my unit testing I finally ended up with the follow refactored code:

func createMultipartFormData(t *testing.T, fieldName, fileName string) (bytes.Buffer, *multipart.Writer) {
    var b bytes.Buffer
    var err error
    w := multipart.NewWriter(&b)
    var fw io.Writer
    file := mustOpen(fileName)
    if fw, err = w.CreateFormFile(fieldName, file.Name()); err != nil {
        t.Errorf("Error creating writer: %v", err)
    }
    if _, err = io.Copy(fw, file); err != nil {
        t.Errorf("Error with io.Copy: %v", err)
    }
    w.Close()
    return b, w
}

func mustOpen(f string) *os.File {
    r, err := os.Open(f)
    if err != nil {
        pwd, _ := os.Getwd()
        fmt.Println("PWD: ", pwd)
        panic(err)
    }
    return r
}

Now it should be pretty easy to use:

    b, w := createMultipartFormData(t, "image","../luke.png")

    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return
    }
    // Don't forget to set the content type, this will contain the boundary.
    req.Header.Set("Content-Type", w.FormDataContentType())

Solution 4

Here is an option that works for files or strings:

package main

import (
   "bytes"
   "io"
   "mime/multipart"
   "os"
   "strings"
)

func createForm(form map[string]string) (string, io.Reader, error) {
   body := new(bytes.Buffer)
   mp := multipart.NewWriter(body)
   defer mp.Close()
   for key, val := range form {
      if strings.HasPrefix(val, "@") {
         val = val[1:]
         file, err := os.Open(val)
         if err != nil { return "", nil, err }
         defer file.Close()
         part, err := mp.CreateFormFile(key, val)
         if err != nil { return "", nil, err }
         io.Copy(part, file)
      } else {
         mp.WriteField(key, val)
      }
   }
   return mp.FormDataContentType(), body, nil
}

Example:

package main
import "net/http"

func main() {
   form := map[string]string{"image": "@IMAGEFILE", "key": "KEY"}
   ct, body, err := createForm(form)
   if err != nil {
      panic(err)
   }
   http.Post("https://stackoverflow.com", ct, body)
}

https://golang.org/pkg/mime/multipart#Writer.WriteField

Share:
121,852
Epitouille
Author by

Epitouille

Updated on June 09, 2021

Comments

  • Epitouille
    Epitouille almost 3 years

    I'm trying to upload images from my computer to a website using go. Usually, I use a bash script that sends a file and a key to the server:

    curl -F "image"=@"IMAGEFILE" -F "key"="KEY" URL
    

    it works fine, but I'm trying to convert this request into my golang program.

    http://matt.aimonetti.net/posts/2013/07/01/golang-multipart-file-upload-example/

    I tried this link and many others, but, for each code that I try, the response from the server is "no image sent", and I've no idea why. If someone knows what's happening with the example above.