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
}
package upload
import (
"errors"
"net/http"
)
type Server struct {
Uploader *Uploader
}
func (s *Server) UploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// Keep at most 8 MiB in memory; larger parts spill to temp files.
if err := r.ParseMultipartForm(8 << 20); err != nil {
http.Error(w, "invalid multipart body", http.StatusBadRequest)
return
}
headers := r.MultipartForm.File["files"]
if len(headers) == 0 {
http.Error(w, "no files provided", http.StatusBadRequest)
return
}
saved := make([]string, 0, len(headers))
for _, fh := range headers {
file, err := fh.Open()
if err != nil {
http.Error(w, "cannot read part", http.StatusBadRequest)
return
}
path, err := s.Uploader.Save(fh.Filename, file)
file.Close()
if err != nil {
s.writeSaveError(w, err)
return
}
saved = append(saved, path)
}
w.WriteHeader(http.StatusCreated)
}
func (s *Server) writeSaveError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrTypeNotAllowed):
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
case errors.Is(err, ErrTooLarge):
http.Error(w, err.Error(), http.StatusRequestEntityTooLarge)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
}
package main
import (
"log"
"net/http"
"example.com/app/upload"
)
func main() {
srv := &upload.Server{
Uploader: &upload.Uploader{
Dir: "/var/data/uploads",
MaxBytes: 25 << 20,
Allowed: map[string]bool{
"image/jpeg": true,
"image/png": true,
"application/pdf": true,
"application/zip": true,
},
},
}
mux := http.NewServeMux()
mux.HandleFunc("/uploads", srv.UploadHandler)
s := &http.Server{
Addr: ":8080",
Handler: mux,
MaxHeaderBytes: 1 << 16,
}
log.Fatal(s.ListenAndServe())
}
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
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
Share this code
Here's the card — post it anywhere.