proxy

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: May 31, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package proxy assembles the httputil.ReverseProxy + custom Transport pipeline documented in ARCHITECTURE § 10.2.

The custom Transport tees req.Body and resp.Body to per-trace capture sinks. Headers are NOT written to disk — they are read at finalize time directly from the http.Request / http.Response structs.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewReverseProxy

func NewReverseProxy(upstream *url.URL, transport http.RoundTripper) *httputil.ReverseProxy

NewReverseProxy builds the httputil.ReverseProxy described in ARCHITECTURE § 10.1 and § 10.4. The returned proxy:

  • Forwards to upstream with FlushInterval = -1 so SSE chunks flush to the client without buffering.
  • Director rewrites req.URL.Scheme/Host and req.Host only; does not touch path, query, or other headers. X-Forwarded-For is suppressed via the nil-assignment trick (Del does NOT work).
  • Uses the provided Transport (typically a CaptureTransport).

On upstream error, the default ReverseProxy.ErrorHandler is replaced with a quiet variant: it writes a 502 to the client and logs nothing (the caller is responsible for trace-level error logging).

func TraceIDFromContext

func TraceIDFromContext(ctx context.Context) string

TraceIDFromContext recovers a trace ID attached by WithTraceID. Returns empty string if absent (which indicates a wiring bug — the handler should always attach a trace ID before calling the proxy).

func WithTraceID

func WithTraceID(ctx context.Context, traceID string) context.Context

WithTraceID attaches a trace ID to a context, retrievable by the custom Transport's RoundTrip via TraceIDFromContext.

Types

type CaptureHandle

type CaptureHandle struct {
	// contains filtered or unexported fields
}

CaptureHandle is an opaque handle to a previously-attached response capture sink. ModifyResponse-style hooks that need to mutate the response body call DetachResponseCapture to remove the tee from resp.Body (so they can read upstream bytes without those bytes landing in the capture sink), then call RewrapBody on whatever they hand back to ReverseProxy so the captured stream reflects what the CLIENT receives — not what upstream emitted.

A zero CaptureHandle is the "no-op" value: DetachResponseCapture returns it when resp.Body wasn't a teeReadCloser (e.g. capture was never wired). RewrapBody on a zero handle returns its argument unchanged.

func DetachResponseCapture

func DetachResponseCapture(resp *http.Response) CaptureHandle

DetachResponseCapture unwraps any *teeReadCloser the CaptureTransport placed on resp.Body and returns a handle holding the underlying sink. resp.Body is updated to point at the raw upstream reader. The returned handle's RewrapBody re-installs the tee on whatever body the caller eventually hands back.

Idempotent: if resp.Body is already raw (or nil), the returned handle is a no-op and resp.Body is left alone.

func (CaptureHandle) RewrapBody

func (h CaptureHandle) RewrapBody(rc io.ReadCloser) io.ReadCloser

RewrapBody returns rc wrapped in a tee that feeds the handle's sink. A zero handle returns rc unchanged so callers can use the helper unconditionally regardless of whether capture was wired.

Pass a nil rc to get nil back (so e.g. an intercept that drops the upstream body can still call this without a special case).

type CaptureTransport

type CaptureTransport struct {
	// Inner is the underlying transport (typically a configured
	// *http.Transport with DisableCompression=true).
	Inner http.RoundTripper
	// Sinks looks up the per-trace sinks at RoundTrip time.
	Sinks SinkLookup
	// Meta receives request/response metadata callbacks. Optional;
	// when nil, the Transport only does body tee.
	Meta MetaCapture
	// OnDialError is invoked when the inner transport returned a
	// non-HTTP error (DNS / TLS / connection refused). Lets the
	// caller bump a counter without leaking the counters dependency
	// into this package. Optional.
	OnDialError func()
}

CaptureTransport wraps an inner http.RoundTripper and tees req.Body / resp.Body to per-trace capture sinks.

Critically: this transport does NOT consume req.Body itself — it wraps req.Body in a teeing ReadCloser so the inner Transport's read drives the tee. Calling req.Write/req.WriteProxy here would consume the body to EOF and the inner RoundTrip would forward an empty body upstream.

func (*CaptureTransport) RoundTrip

func (t *CaptureTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip wires the body tees and forwards via the inner transport.

On error from the inner transport, the response side never produced anything; the resp.Body wrap is not applied. The trace's finalize will trigger via the error path.

type MetaCapture

type MetaCapture interface {
	OnReqHeaders(traceID string, h http.Header)
	OnRespMeta(traceID string, statusCode int, h http.Header)
}

MetaCapture is an optional hook that receives the outbound request headers (post-Director) and the upstream response status + headers as soon as they are known. Headers are passed as clones; callers may retain them safely. Used by the forwarding handler to populate the trace.Body objects at finalize.

type SinkLookup

type SinkLookup interface {
	// SinksFor returns the req/resp capture sinks for traceID. Both are
	// non-nil when the trace is active; both are nil if the trace has
	// been finalized (in which case the Transport silently drops capture).
	SinksFor(traceID string) (req, resp *capture.Sink)
}

SinkLookup is how the custom Transport finds the right sinks for a given trace. The forwarding handler registers sinks at request start and removes them at finalize.

type StreamWatchdog

type StreamWatchdog struct {
	// contains filtered or unexported fields
}

StreamWatchdog implements the per-stream idle timeout described in ARCHITECTURE § 10.7. A timer is reset on each forwarded chunk via Pulse(); if no chunk arrives for `timeout`, the watchdog cancels the request context. That unblocks the ReverseProxy body copy (which sees ctx cancellation), bubbles up an error to ServeHTTP, and lets finalize set disconnected: true.

The watchdog is "one shot": once it fires, further Pulse calls are no-ops. After finalize, callers should Stop() to release the underlying timer.

func NewStreamWatchdog

func NewStreamWatchdog(cancel context.CancelFunc, timeout time.Duration) *StreamWatchdog

NewStreamWatchdog starts the timer immediately. If `timeout` is zero or negative, returns nil — caller treats nil as "disabled".

func (*StreamWatchdog) Fired

func (w *StreamWatchdog) Fired() bool

Fired reports whether the watchdog triggered (timeout expired and the context cancellation has been invoked).

func (*StreamWatchdog) Pulse

func (w *StreamWatchdog) Pulse()

Pulse resets the timer. Safe for concurrent calls. No-op after Stop or fire. Designed to be cheap enough to call on every chunk Write.

func (*StreamWatchdog) Stop

func (w *StreamWatchdog) Stop()

Stop disarms the watchdog. Idempotent.

Jump to

Keyboard shortcuts

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