Handle file uploading with go

11,718

I managed to solve my problem, so here it is in case someone else needs it. And thanks @JiangYD for the tip of using curl to test the server.

TL;DR

  • I wrote http.HandleFunc("/submit/", submit) but I was making a POST request to /submit (note the missing slash) << This is important because of redirections
  • Don't specify the Content-Type yourself, the browser will do it for you

LONG ANSWER

I did as @JiangYD said and used curl to test the server, I updated my answer with the response. I found odd that there was a 301 Redirect since I didn't put it there, I decided to use the following curl command

curl -v -F 'uploadFile=@\"C:/Users/raul-/Desktop/test.png\"' -L http://localhost:8080/submit

(note the -L) That way curl followed the redirect, though it failed again because, when redirecting, curl switched from POST to GET but with that response I found out that the request to /submit was being redirected to /submit/ and I remembered that's how I wrote it in the main function.

After fixing that it still failed, the response was http: no such file and by looking at the net/http code I found that it meant the field didn't exist, so I did a quick test iterating over all the field names obtained:

for k, _ := range r.MultipartForm.File {
    log.Println(k)
}

I was getting 'uploadFile as the field name, I removed the single quotes in the curl command and now it uploaded the file perfectly

But it doesn't end here, I now knew the server was working correctly because I could upload a file using curl but when I tried uploading it through the hosted web page I got an error: no multipart boundary param in Content-Type.

So I found out I was suppose to include the boundary in the header, I changed fetch to something like this:

fetch('/submit', {
    method: 'post',
    headers: {
        "Content-Type": "multipart/form-data; boundary=------------------------" + boundary
    }, body: formData})

I calculate the boundary like this:

var boundary = Math.random().toString().substr(2);

But I still got an error: multipart: NextPart: EOF So how do you calculate the boundary? I read the spec https://html.spec.whatwg.org/multipage/forms.html#multipart/form-data-encoding-algorithm and found out the boundary is calculated by the algorithm that encodes the file, which in my case is FormData, the FormData API doesn't expose a way to get that boundary but I found out that the browser adds the Content-Type with multipart/form-data and the boundary automatically if you don't specify it so I removed the headers object from the fetch call and now it finally works!

Share:
11,718
redsalt
Author by

redsalt

Updated on July 24, 2022

Comments

  • redsalt
    redsalt almost 2 years

    I've started playing with go very recently so I'm still a noob, sorry if I make too many mistakes. I've been trying to fix this for a long time but I just don't understand what's going on. In my main.go file I have a main function:

    func main() {
        http.HandleFunc("/", handler)
        http.HandleFunc("/submit/", submit)
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    

    The handler function looks like this:

    func handler(w http.ResponseWriter, r *http.Request) {
        data, _ := ioutil.ReadFile("web/index.html")
        w.Write(data)
    }
    

    I know this is not the best way to serve a website The submit function looks like this:

    func submit(w http.ResponseWriter, r *http.Request) {
        log.Println("METHOD IS " + r.Method + " AND CONTENT-TYPE IS " + r.Header.Get("Content-Type"))
        r.ParseMultipartForm(32 << 20)
        file, header, err := r.FormFile("uploadFile")
        if err != nil {
            json.NewEncoder(w).Encode(Response{err.Error(), true})
            return
        }
        defer file.Close()
    
        out, err := os.Create("/tmp/file_" + time.Now().String() + ".png")
        if err != nil {
            json.NewEncoder(w).Encode(Response{err.Error(), true})
            return
        }
        defer out.Close()
    
        _, err = io.Copy(out, file)
        if err != nil {
            json.NewEncoder(w).Encode(Response{err.Error(), true})
            return
        }
    
        json.NewEncoder(w).Encode(Response{"File '" + header.Filename + "' submited successfully", false})
    }
    

    The problem is when the submit function is executed, r.Method is GET and r.Header.Get("Content-Type") is an empty string, then it continues until the first if where r.FormFile returns the following error: request Content-Type isn't multipart/form-data I don't understand why r.Method is always GET and there's no Content-Type. I've tried to do the index.html in many different ways but r.Method is always GET and Content-Type is empty. Here's the function in index.html that uploads a file:

    function upload() {
        var formData = new FormData();
        formData.append('uploadFile', document.querySelector('#file-input').files[0]);
        fetch('/submit', {
            method: 'post',
            headers: {
              "Content-Type": "multipart/form-data"
            },
            body: formData
        }).then(function json(response) {
            return response.json()
        }).then(function(data) {
            window.console.log('Request succeeded with JSON response', data);
        }).catch(function(error) {
            window.console.log('Request failed', error);
        });
    }
    

    And here's the HTML:

    <input id="file-input" type="file" name="uploadFile" />
    

    Note that the tag is not inside a tag, I thought that could be the problem so I changed both the function and the HTML to something like this:

    function upload() {
        fetch('/submit', {
            method: 'post',
            headers: {
              "Content-Type": "multipart/form-data"
            },
            body: new FormData(document.querySelector('#form')
        }).then(function json(response) {
            return response.json()
        }).then(function(data) {
            window.console.log('Request succeeded with JSON response', data);
        }).catch(function(error) {
            window.console.log('Request failed', error);
        });
    }
    
    <form id="form" method="post" enctype="multipart/form-data" action="/submit"><input id="file-input" type="file" name="uploadFile" /></form>
    

    But that didn't work neither. I've searched with Google how to use fetch() and how to receive a file upload from go and I've seen that they are pretty similar to mine, I don't know what I'm doing wrong.

    UPDATE: After using curl -v -F 'uploadFile=@\"C:/Users/raul-/Desktop/test.png\"' http://localhost:8080/submit I get the following output:

    * Trying ::1...
    * Connected to localhost (::1) port 8080 (#0)
    > POST /submit HTTP/1.1
    > Host: localhost:8080
    > User-Agent: curl/7.45.0
    > Accept: */*
    > Content-Length: 522
    > Expect: 100-continue
    > Content-Type: multipart/form-data; boundary=---------------------------a17d4e54fcec53f8
    >
    < HTTP/1.1 301 Moved Permanently
    < Location: /submit/
    < Date: Wed, 18 Nov 2015 14:48:38 GMT
    < Content-Length: 0
    < Content-Type: text/plain; charset=utf-8
    * HTTP error before end of send, stop sending
    <
    * Closing connection 0
    

    The console where I'm running go run main.go outputs nothing when using curl.

  • darkdefender27
    darkdefender27 over 6 years
    Thanks for the insight. Helped me a lot.
  • Odd
    Odd about 3 years
    "note the missing slash" saved my day. Please emphasise your note as this might often be the culprit- +1