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")
}
}
package upload
import (
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
)
const maxFileBytes = 500 << 20
type savedFile struct {
Path string
Size int64
}
func sanitizeName(name string) string {
name = filepath.Base(name)
name = strings.ReplaceAll(name, "/", "")
name = strings.ReplaceAll(name, "\\", "")
if name == "" || name == "." || name == ".." {
return "upload.bin"
}
return name
}
func saveStreaming(part *multipart.Part, dir string) (savedFile, error) {
dst := filepath.Join(dir, sanitizeName(part.FileName()))
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o640)
if err != nil {
return savedFile{}, fmt.Errorf("create dest: %w", err)
}
success := false
defer func() {
out.Close()
if !success {
os.Remove(dst)
}
}()
limited := io.LimitReader(part, maxFileBytes)
n, err := io.Copy(out, limited)
if err != nil {
return savedFile{}, fmt.Errorf("stream copy: %w", err)
}
if err := out.Sync(); err != nil {
return savedFile{}, fmt.Errorf("fsync: %w", err)
}
success = true
return savedFile{Path: dst, Size: n}, nil
}
package main
import (
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/upload", uploadHandler)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
// Generous write/read timeouts, but bounded so slow
// clients cannot hold connections open indefinitely.
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 0, // large uploads need unbounded body read
WriteTimeout: 15 * time.Minute,
IdleTimeout: 60 * time.Second,
}
log.Println("listening on :8080")
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
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
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
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
Share this code
Here's the card — post it anywhere.