internal/parser/http1/response.go

package http1

import (
	"bufio"
	"errors"
	"fmt"
	"strconv"
	"strings"
)

// Response is the decoded form of an HTTP/1.x response including body.
type Response struct {
	Version    string
	Status     int
	Reason     string
	Headers    [][2]string
	Body       []byte
	Trailers   [][2]string
	Incomplete bool
}

var (
	errBadStatus = errors.New("http1: status code out of range")
)

// ReadResponse parses one response from r. Unlike ReadRequest, the body
// framing can depend on the request method (e.g. HEAD has none); callers
// that know this can bypass body reading by passing isHEAD=true via the
// Decoder pair instead of calling ReadResponse directly.
func ReadResponse(r *bufio.Reader) (*Response, error) {
	line, err := readCRLF(r)
	if err != nil {
		return nil, err
	}
	resp, err := parseStatusLine(line)
	if err != nil {
		return nil, err
	}
	h, err := ReadHeaders(r)
	if err != nil {
		return nil, err
	}
	resp.Headers = h
	body, trailers, err := ReadBody(r, h, true)
	if err != nil {
		return nil, err
	}
	resp.Body = body
	resp.Trailers = trailers
	return resp, nil
}

// parseStatusLine reads "HTTP/1.1 SP STATUS SP REASON".
func parseStatusLine(line string) (*Response, error) {
	line = strings.TrimRight(line, "\r\n")
	parts := strings.SplitN(line, " ", 3)
	if len(parts) < 2 {
		return nil, fmt.Errorf("http1: bad status line: %q", line)
	}
	version := parts[0]
	if !strings.HasPrefix(version, "HTTP/1.") {
		return nil, fmt.Errorf("http1: bad version in status: %q", version)
	}
	code, err := strconv.Atoi(strings.TrimSpace(parts[1]))
	if err != nil {
		return nil, fmt.Errorf("http1: bad status code %q: %w", parts[1], err)
	}
	if code < 100 || code > 599 {
		return nil, fmt.Errorf("%w: %d", errBadStatus, code)
	}
	reason := ""
	if len(parts) == 3 {
		reason = parts[2]
	}
	return &Response{
		Version: version,
		Status:  code,
		Reason:  reason,
	}, nil
}

// StatusClass maps a numeric code to its 1xx/2xx/3xx/4xx/5xx bucket.
func StatusClass(code int) string {
	switch {
	case code >= 100 && code < 200:
		return "informational"
	case code < 300:
		return "success"
	case code < 400:
		return "redirect"
	case code < 500:
		return "client-error"
	case code < 600:
		return "server-error"
	default:
		return "unknown"
	}
}