Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle file uploading with go

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.

like image 867
redsalt Avatar asked Nov 18 '15 03:11

redsalt


People also ask

How do I upload a file to go?

Go to localhost:8080/upload, and you will see a form to upload a file. After selecting a file and clicking upload, the file should be created in your local filesystem.

What is ParseMultipartForm?

MaxBytesReader() method is used to limit the size of incoming request bodies. For single file uploads, limiting the size of the request body provides a good approximation of limiting the file size. The ParseMultipartForm() method subsequently parses the request body as multipart/form-data up to the max memory argument.

How do I upload on curl?

Send the data using curl's –d or –data option. To execute a CURL file upload, you need to use the –d command-line option and begin data with the @ symbol. The file's name should come after the data with @ symbol so CURL can read it and send it to the server.


1 Answers

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!

like image 88
redsalt Avatar answered Oct 31 '22 15:10

redsalt