go 142 lines · 3 tabs

Streaming Multipart File Uploads to Disk in Go net/http

Shared by codesnips Jul 2026
3 tabs
package upload

import (
	"errors"
	"io"
	"log"
	"net/http"
)

const maxUploadBytes = 500 << 20 // 500 MiB per request

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes)

	mr, err := r.MultipartReader()
	if err != nil {
		http.Error(w, "expected multipart/form-data", http.StatusBadRequest)
		return
	}

	var saved []savedFile
	for {
		part, err := mr.NextPart()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			http.Error(w, "malformed upload", http.StatusBadRequest)
			return
		}

		if part.FileName() == "" {
			// A plain form field; drain and ignore.
			io.Copy(io.Discard, part)
			part.Close()
			continue
		}

		f, err := saveStreaming(part, "/var/uploads")
		part.Close()
		if err != nil {
			log.Printf("save %q: %v", part.FileName(), err)
			http.Error(w, "upload failed", http.StatusInternalServerError)
			return
		}
		saved = append(saved, f)
	}

	w.WriteHeader(http.StatusCreated)
	for _, f := range saved {
		io.WriteString(w, f.Path+"\n")
	}
}
3 files · go Explain with highlit

This snippet shows how to accept a multipart file upload in a plain net/http server and stream it straight to disk without buffering the whole file in memory. The naive approach, r.ParseMultipartForm, loads the request into memory up to a threshold and spills the rest to temp files, which is wasteful for large uploads and gives little control. Instead, uploadHandler in upload_handler.go uses r.MultipartReader() to walk the request part by part, copying each file straight through with io.Copy.

The handler first guards the request: it rejects non-POST methods and wraps the body in http.MaxBytesReader so a client cannot exhaust disk by sending an unbounded stream. Calling r.MultipartReader() returns an error if the Content-Type is not multipart/form-data, so no explicit content-type check is needed. The loop then pulls parts with mr.NextPart(); each part is either a form field or a file, distinguished by whether part.FileName() is empty. File parts are handed to saveStreaming.

In save_file.go, saveStreaming performs the real work. Filenames from clients are untrusted, so sanitizeName runs the name through filepath.Base and strips path separators to defeat directory traversal like ../../etc/passwd. The destination file is created with os.O_CREATE|os.O_EXCL so an attacker cannot overwrite an existing file by reusing a name. The copy itself is bounded by an io.LimitReader as a second line of defense, and io.Copy moves data in fixed-size chunks, giving natural backpressure: reads from the socket only advance as fast as writes to disk complete.

Cleanup is handled carefully. A success flag combined with a deferred closure removes the partially written file if io.Copy fails midway, so failed uploads never leave corrupt artifacts behind. The returned savedFile struct records the final path and byte count.

The main.go tab wires the handler into a http.Server with real timeouts, which matters for upload endpoints because a slow client holding a connection open is a common denial-of-service vector. This pattern is the right choice whenever uploads can be large, when memory pressure matters, or when validation must happen on a per-part basis rather than after the whole body is buffered.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming Multipart File Uploads to Disk in Go net/http — share card
Link copied