Uploading images (React + Go)

This article outlines the basic front- and backend flow of adding image uploading functionality to a web project using Javascript (React) and Go. It also addresses some additional features like file size limiting, content type limiting and sending files together with other input like text (adding a new forum post). This article does not touch setting up a React project or a server in Go.

Starting from the beginning, we first have to enable the user to upload a picture from the UI. This is done by using the type=”file” of the input tag:

<input type="file" name="Image" onChange={uploadHander} accept="image/jpeg" />

File type input automatically inserts a button that takes the user to the local file system. Event listeners can be added as usual. Optionally, an accept attribute can be used to restrict the file types that are available to be selected. Access attribute accepts MIME types as a comma separated list. It only makes other filetypes unclickable for the user in the UI when selecting a file. File types should still be checked in the backend, too.

The selected image is stored as a FileList object containing a list of File objects. The selected file(s) can be accessed like an array as follows:

handleSetImage = (event) => {
    this.setState({
      image: event.target.files[0]
    })
}

Next, we send the selected file to the backend using the fetch API:

uploadHandler = () => {
  const formData = new FormData();
  formData.append('Image', this.state.image);
  formData.append('Text', this.state.text);
  formData.append('Title', this.state.title);

  fetch(
    '/api/post/',
    {
      method: 'POST',
      body: formData,
    })
      .then((response) => {
          // Check response code
          return response.json();
      })
      .then((data) => {
        if (data.message == "http: request body too large") {
          data.message = "File too big, max size is 20 MB"
        } else {
        // HandleData
        }
      })
      .catch(() => {
        // Handle errors
      });
};

For storing the data, we use Javascript’s native FormData interface. This interface enables storing different types of data from form fields as key-value pairs. Files can be added by using the append method.

The encoding for uploading files usually needs to be multipart/form-data, but when using the fetch API, Content-Type needs to be omitted entirely for everything to work properly (the fetch API does the necessary work in the background). FormData is sent as request body. Here we also check for a specific "http: request body too large" error sent by the backend and make the error text more detailed for the user.

This is all the work that is needed from the front-end side. In the backend, we are accepting POST requests in a createPost handler that returns the status code and a (error or success) message to the frontend. I will not go into detail about the server setup here.

The first important thing we need to check in the backend is file size. We don’t want a careless or a malicious user to exhaust our server resources with a huge upload. Go offers a simple way to check for file size.

maxSize := 21 << 20
r.Body = http.MaxBytesReader(w, r.Body, int64(maxSize))

MaxBytesReader reads the request body and if the body is bigger than the desired maxSize (21 MB here), it returns a "http: request body too large" error.

Next, we have to parse the incoming form, to make it accessible:

err := r.ParseMultipartForm(maxMemory)
if err != nil {
    // Error handling
}

File size can also be checked in ParseMultipartForm, but this alone would not be enough. The maxMemory param only sets the maximum amount of data that can be read to memory, but the rest of the body is still read to the end and stored in temporary files.

ParseMultiPartForm makes the data available by populating request.Form and request.PostForm. These sore data as a slice. For accessing the direct value, use request.PostFormValue(Key). Files become accessible through request.FormFile , that return a file (type multipart.File), a file header (type *multipart.FileHeader) and an error:

type postForm struct {
  Title        string
  Text         string
  ImagePath    string
}

p := postForm{}

p.Title = r.PostFormValue("Title")
p.Text = r.PostFormValue("Text")

img, imgHeader, err := r.FormFile("Image")
if err != nil {
  // check if a file is sent.
  if err.Error() == "http: no such file" {
    // Error handling
  } else {
    // handle other errors
}

defer img.Close()

If there is no file attached, r.FormFile returns a "http: no such file" error. The header gives access to file's attributes like the size and name. The size can used for an additional check for file size (before, we checked the whole body, not the file specifically, so there could still theoretically be files that are a bit over the desired limit).

if imgHeader.Size > 20 << 20 {
  // error handling
}

We now have the image in a variable. Files are best stored on the file system, not the database. If dealing with forum posts, attached files can be referenced in the DB by file path.

fileName := fmt.Sprintf("./uploads/%d%s", time.Now().UnixNano(), filepath.Ext(imgHandler.Filename))

dst, err := os.Create(fileName)
if err != nil {
  // error handling
}
defer dst.Close()

_, err = io.Copy(dst, img)
if err != nil {
  // error handling
}

To save a file, we need to first create it and the copy it to our desired destination using os.Create and io.Copy. Here we create a unique filename by using creation time. filepath.Ext appends the correct filename extension to the end.

Before doing this, however, we should check if the filetypes are correct. For that, we read the file’s beginning into a buffer and then test it with a http package’s function DetectContentType. After reading the file, we return the pointer to the start for further reading by using img.Seek.

buff := make([]byte, 512)
_, err = img.Read(buff)
if err != nil {
  // error handling
}
            
filetype := http.DetectContentType(buff)
if filetype != "image/jpeg" {
  err = errors.New("Wrong file type. Only jpeg accepted")
}

_, err = img.Seek(0, io.SeekStart)
if err != nil {
  // error handling
}

Now we have validated our file and saved it to our desired destination. To serve the files to frontend, we can use a http.FileServer.

imageServer := http.FileServer(http.Dir("./uploads"))
http.Handle("/uploads/", http.StripPrefix("/uploads/", imageServer))

To display the files on the frontend, an img tag with a correct src attribute is all that is needed. In the case of a forum post, the post can also come without an image. To handle that, we can add an image state and render the image tag conditionally only if a post has an image attached.

{this.state.image && <img src={this.state.ImagePath}  alt="Post image" width="200" height="200"/>}

And this is it. We now have a basic workflow for uploading images and displaying them. Hope the post was helpful!