Frontend Labs
⋯

Day 16 — io.Reader & io.Writer

Learning Objectives

  • Compose readers and writers to inspect traffic without buffering it all
  • Use io.TeeReader to mirror traffic — the foundation of your TCP traffic inspector (Week 5)
  • Use io.LimitReader to enforce body size limits — protection against request body attacks

Key Concepts

io.Reader interface:
    Read(p []byte) (n int, err error)

io.Writer interface:
    Write(p []byte) (n int, err error)

Implementations: net.Conn, os.File, bytes.Buffer, strings.Reader,
                 http.Request.Body, gzip.Reader, tls.Conn

Composing:
    io.TeeReader(r, w)    — reads from r, copies every byte to w
    io.LimitReader(r, n)  — reads at most n bytes, then returns io.EOF
    io.MultiWriter(w1,w2) — writes to all writers simultaneously
    io.Copy(dst, src)     — copy all bytes; returns (n, err)
    bufio.NewReader(r)    — adds buffering; enables ReadLine, ReadString

Exercise: Request Body Inspector (WAF Foundation)

This is the core of your WAF body scanner (Day 72):

package main

import (
    "bytes"
    "fmt"
    "io"
    "strings"
)

const maxBodySize = 10 * 1024 // 10KB

// InspectBody reads a request body, enforces size limit, logs it,
// and returns a new reader that can be read again by the downstream handler.
// This solves the "you can only read a body once" problem.
func InspectBody(body io.ReadCloser, log io.Writer) (io.ReadCloser, int64, error) {
    if body == nil {
        return http.NoBody, 0, nil
    }

    // LimitReader: stop reading after maxBodySize (attacker can't force OOM)
    limited := io.LimitReader(body, maxBodySize+1)

    // TeeReader: every byte read from 'limited' is also written to 'log'
    var buf bytes.Buffer
    tee := io.TeeReader(limited, &buf)

    // Read all — this drains the original body
    n, err := io.Copy(io.Discard, tee)
    body.Close()

    if n > maxBodySize {
        return nil, n, fmt.Errorf("request body exceeds %d bytes", maxBodySize)
    }

    // Log the body content (buf was written to by TeeReader)
    fmt.Fprintf(log, "BODY[%d bytes]: %s\n", buf.Len(), buf.String())

    // Wrap the buffer as a new ReadCloser for downstream handler
    return io.NopCloser(bytes.NewReader(buf.Bytes())), n, err
}

func main() {
    // Simulate an HTTP request body
    originalBody := io.NopCloser(strings.NewReader(`{"username":"admin","password":"secret"}`))

    var auditLog strings.Builder
    newBody, size, err := InspectBody(originalBody, &auditLog)
    if err != nil {
        fmt.Println("ERROR:", err)
        return
    }

    fmt.Printf("Inspected %d bytes\n", size)
    fmt.Printf("Audit log: %s\n", auditLog.String())

    // Downstream handler reads the body again — works because we restored it
    data, _ := io.ReadAll(newBody)
    fmt.Printf("Handler got: %s\n", data)
}

Real-World Context

io.TeeReader is used by AWS WAF, Cloudflare, and ModSecurity to inspect request bodies. The critical requirement: the body must be readable by the WAF AND by the application handler. Without io.NopCloser(bytes.NewReader(...)), the body would be empty when it reaches your route handler.


Generated from week3.md · 8 Oct 2026 16:36