doppelgangerreader

package module
v1.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 6 Imported by: 1

README

DoppelgangerReader

CI PkgGoDev go-report

DoppelgangerReader provides a way to read one io.Reader multiple times.

An io.Reader can normally only be consumed once. A DoppelgangerFactory wraps a reader and hands out any number of independent readers over the same bytes, buffering only as much as it needs to keep them in sync.

Install

go get github.com/Eun/go-doppelgangerreader

Usage

package main

import (
	"bytes"
	"fmt"
	"io"

	"github.com/Eun/go-doppelgangerreader"
)

func main() {
	reader := bytes.NewBufferString("Hello World")
	factory := doppelgangerreader.NewFactory(reader)
	defer factory.Close()

	d1 := factory.NewDoppelganger()
	defer d1.Close()
	fmt.Println(io.ReadAll(d1))

	d2 := factory.NewDoppelganger()
	defer d2.Close()
	fmt.Println(io.ReadAll(d2))
}

Readers are independent: each one sees the full stream regardless of what the others have already consumed, and in any order.

HTTP middleware

The common case is an HTTP body that has to be read more than once — for example verifying a signature over the raw bytes while also decoding it as JSON. HTTPMiddleware installs a factory on the request and replaces the body with a doppelganger:

handler := doppelgangerreader.HTTPMiddleware(next, 1<<20) // 1 MiB limit, 0 = unlimited

func next(w http.ResponseWriter, r *http.Request) {
	factory := doppelgangerreader.HTTPBodyFactory(r)

	// the raw bytes, for a signature check
	raw := factory.NewDoppelganger()
	defer raw.Close()

	// r.Body is still readable, for the usual decoding
	var payload map[string]any
	_ = json.NewDecoder(r.Body).Decode(&payload)
}

A body larger than the limit is reported as BodyTooLargeError on the next read rather than being silently truncated, so a handler can never mistake a truncated body for a complete one.

Errors

Every error is a concrete type, and all of them work with errors.Is and errors.As, including through wrapping:

Error Reported when
NilReaderError the reader to mimic is nil
ReaderNotFoundError the reader is not (or no longer) registered with the factory
NotAReaderInstanceError the reader was not created by a DoppelgangerFactory
BodyTooLargeError the HTTP body exceeded the HTTPMiddleware limit
if errors.Is(err, doppelgangerreader.BodyTooLargeError{}) {
	// ...
}

// errors.As when you need the detail
var tooLarge doppelgangerreader.BodyTooLargeError
if errors.As(err, &tooLarge) {
	log.Printf("limit was %d bytes", tooLarge.Limit)
}

Each also has an Is…Error(err) helper (IsNilReaderError, and so on) for callers who prefer that style; they are built on errors.As, so they match wrapped errors too.

Development

mise install          # install the pinned Go and golangci-lint
mise run test         # tests with coverage -> coverage.out
mise run test:race    # tests under the race detector
mise run lint         # golangci-lint
Prerequisites
  • mise — manages tool versions and tasks

Tool versions are pinned in mise.toml.

Mise tasks
Task Description
mise run test Run tests with coverage
mise run test:race Run tests with the race detector
mise run lint Run golangci-lint
mise run clean Remove build artifacts
CI

CI logic is driven by mise — the GitHub Actions workflows only check out, install mise, and run tasks.

Workflow Trigger What it does
ci.yml Push + PR Lint, vulnerability scan (nancy), tests with coverage on Linux/macOS/Windows, race detector, coverage PR comment
pr_labeler.yml PR opened/edited Labels PRs from their title
release_draft.yml Push to default branch Drafts release notes

The race detector runs as its own job because the package hands out concurrent readers over a single source, so it is part of the contract rather than an optional extra.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func HTTPMiddleware

func HTTPMiddleware(handler http.Handler, limit int64) http.Handler

HTTPMiddleware adds a doppelganger factory for the body to the request. At the same time it replaces the original body with a doppelganger reader. You can specify a size limit for the reader (0 disables the limit) The factory can be fetched by using HTTPBodyFactory()

A body that exceeds the limit is reported as BodyTooLargeError on the next Read rather than being silently truncated, so a handler cannot mistake a truncated body for a complete one. Use IsBodyTooLargeError to detect it.

func IsBodyTooLargeError

func IsBodyTooLargeError(e error) bool

IsBodyTooLargeError returns true if the specified error is a BodyTooLargeError.

func IsNilReaderError

func IsNilReaderError(e error) bool

IsNilReaderError returns true if the specified error is a NilReaderError.

func IsNotAReaderInstanceError

func IsNotAReaderInstanceError(e error) bool

IsNotAReaderInstanceError returns true if the specified error is a NotAReaderInstanceError.

func IsReaderNotFoundError

func IsReaderNotFoundError(e error) bool

IsReaderNotFoundError returns true if the specified error is a ReaderNotFoundError.

Types

type BodyTooLargeError

type BodyTooLargeError struct {
	// Limit is the configured maximum, in bytes.
	Limit int64
}

BodyTooLargeError is reported when the request body exceeds the limit given to HTTPMiddleware.

func (BodyTooLargeError) Error

func (e BodyTooLargeError) Error() string

Error returns the error message.

func (BodyTooLargeError) Is

func (BodyTooLargeError) Is(target error) bool

Is reports whether target is a BodyTooLargeError, so errors.Is works on wrapped errors.

Limit is deliberately not compared: without this method errors.Is falls back to struct equality, so the natural check errors.Is(err, BodyTooLargeError{}) would be false for any non-zero limit. Use errors.As when the limit itself is needed.

type DoppelgangerFactory

type DoppelgangerFactory interface {
	NewDoppelganger() io.ReadCloser
	RemoveDoppelganger(r io.ReadCloser) error
	Close() error
}

DoppelgangerFactory is a reader that mimics the behavior of an other reader it can be used to read readers multiple times.

Example (HttpErrorHandler)
package main

import (
	"net/http"
	"time"

	"io"
	"log"

	"github.com/Eun/go-doppelgangerreader"
)

type errorHandler struct {
	NextHandler http.Handler
}

func (e errorHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	factory := doppelgangerreader.NewFactory(request.Body)
	request.Body = factory.NewDoppelganger()
	defer func() {
		err := recover()
		body, _ := io.ReadAll(io.LimitReader(factory.NewDoppelganger(), 128))
		log.Printf("handler panic: %#v, body was %v", err, body)
		factory.Close()
	}()
	e.NextHandler.ServeHTTP(writer, request)
}

func main() {
	handler := http.NewServeMux()
	handler.HandleFunc("/", func(_ http.ResponseWriter, request *http.Request) {
		_, _ = io.ReadAll(request.Body)
		panic("some random error")
	})

	srv := &http.Server{
		Addr:              ":8000",
		Handler:           errorHandler{handler},
		ReadHeaderTimeout: 10 * time.Second,
	}
	_ = srv.ListenAndServe()
}

func GetFactory

func GetFactory(reader io.Reader) DoppelgangerFactory

GetFactory returns the DoppelgangerFactory if the reader is a Doppelganger.

func HTTPBodyFactory

func HTTPBodyFactory(r *http.Request) DoppelgangerFactory

HTTPBodyFactory returns a http body factory for a request. Notice that you have to use the HTTPMiddleware function.

func NewFactory

func NewFactory(readerToMimic io.Reader) DoppelgangerFactory

NewFactory creates a new DoppelgangerFactory with the original reader specified if the reader is already a Doppelganger it will return the original factory.

type NilReaderError

type NilReaderError struct{}

NilReaderError will be reported if the provided reader is nil.

func (NilReaderError) Error

func (NilReaderError) Error() string

Error returns the error message.

func (NilReaderError) Is

func (NilReaderError) Is(target error) bool

Is reports whether target is a NilReaderError, so errors.Is works on wrapped errors.

type NotAReaderInstanceError

type NotAReaderInstanceError struct{}

NotAReaderInstanceError will be reported if the reader was not created by a DoppelgangerFactory.

func (NotAReaderInstanceError) Error

Error returns the error message.

func (NotAReaderInstanceError) Is

func (NotAReaderInstanceError) Is(target error) bool

Is reports whether target is a NotAReaderInstanceError, so errors.Is works on wrapped errors.

type ReaderNotFoundError

type ReaderNotFoundError struct{}

ReaderNotFoundError will be reported if the reader is not (or no longer) registered with the factory. A reader that has been closed, removed, or whose factory has already closed is no longer registered, so this is not necessarily a caller error.

func (ReaderNotFoundError) Error

func (ReaderNotFoundError) Error() string

Error returns the error message.

func (ReaderNotFoundError) Is

func (ReaderNotFoundError) Is(target error) bool

Is reports whether target is a ReaderNotFoundError, so errors.Is works on wrapped errors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL