go 152 lines · 3 tabs

Streaming Multipart Upload Handler With MIME Sniffing in Go

Shared by codesnips Aug 2026
3 tabs
package upload

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"os"
	"path/filepath"
)

var (
	ErrTypeNotAllowed = errors.New("upload: content type not allowed")
	ErrTooLarge       = errors.New("upload: file exceeds size limit")
)

type Uploader struct {
	Dir      string
	MaxBytes int64
	Allowed  map[string]bool
}

func (u *Uploader) Save(name string, src io.Reader) (string, error) {
	br := bufio.NewReader(src)

	head, err := br.Peek(512)
	if err != nil && err != io.EOF && err != bufio.ErrBufferFull {
		return "", err
	}

	detected := http.DetectContentType(head)
	mediaType, _, err := mime.ParseMediaType(detected)
	if err != nil {
		return "", fmt.Errorf("parse media type: %w", err)
	}
	if !u.Allowed[mediaType] {
		return "", fmt.Errorf("%w: %s", ErrTypeNotAllowed, mediaType)
	}

	dstPath := filepath.Join(u.Dir, filepath.Base(name))
	dst, err := os.Create(dstPath)
	if err != nil {
		return "", err
	}
	defer dst.Close()

	limited := io.LimitReader(br, u.MaxBytes+1)
	written, err := io.Copy(dst, limited)
	if err != nil {
		os.Remove(dstPath)
		return "", err
	}
	if written > u.MaxBytes {
		os.Remove(dstPath)
		return "", ErrTooLarge
	}
	return dstPath, nil
}
3 files · go Explain with highlit

This snippet shows how a Go HTTP service accepts multipart file uploads, verifies the real content type of each part by sniffing bytes rather than trusting the client, and streams the data to disk without buffering whole files in memory. The core idea is that the Content-Type header and file extension supplied by a browser are attacker-controlled and unreliable, so the server independently determines the type from the first 512 bytes using http.DetectContentType, exactly the number of bytes the WHATWG sniffing algorithm inspects.

In uploader.go, the Uploader struct captures policy: a whitelist of allowed MIME types and a per-file byte limit. Save peeks the leading 512 bytes with a bufio.Reader so the same bytes can be handed to http.DetectContentType and then replayed into the destination file. Because bufio.Reader.Peek does not consume the stream, no data is lost — the reader is later drained in full via io.Copy. The detected type is normalized with mime.ParseMediaType to strip any ; charset=... parameters before it is checked against the allow-list, and an io.LimitReader caps the copy so an oversized upload is rejected instead of filling the disk.

In handler.go, UploadHandler calls r.ParseMultipartForm with a small in-memory threshold; anything larger spills to temp files managed by the standard library. Iterating r.MultipartForm.File["files"] yields *multipart.FileHeader values whose Open method returns a multipart.File. Each part is passed to the Uploader, and typed sentinel errors (ErrTypeNotAllowed, ErrTooLarge) are mapped to 400 Bytes responses while unexpected failures become 500s.

The main trade-off is that sniffing only reads a prefix, so it cannot distinguish types that share magic bytes (many Office formats look like ZIP) — the allow-list should account for that. Streaming with io.Copy keeps memory flat regardless of file size, which matters under concurrent uploads. The defer file.Close() and defer dst.Close() calls, plus cleaning the base filename with filepath.Base to defeat path traversal, are the small details that separate a demo from a handler safe to expose. This pattern is the right reach whenever untrusted binary content enters a system.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming Multipart Upload Handler With MIME Sniffing in Go — share card
Link copied