Tencent/WeKnora · error

outbound request blocked: base transport is required

Error message

outbound request blocked: base transport is required

What it means

SSRFValidatingRoundTripper.RoundTrip refuses to send the request because the wrapper's Base transport field is nil. The round tripper is purely a validation/delegation layer; without an underlying http.RoundTripper there is nowhere to forward the request, so the library fails fast instead of panicking with a nil pointer dereference. It is a construction/initialization bug, not a network or policy problem.

Source

Thrown at internal/utils/security.go:749

		return nil
	}
}

// SSRFValidatingRoundTripper enforces the URL policy for every outbound
// request, including URLs discovered at runtime by SDKs (for example OAuth
// metadata) that never passed through an application handler. Dial-time checks
// remain necessary to pin DNS answers and cover transports that cannot accept
// this wrapper directly.
type SSRFValidatingRoundTripper struct {
	Base http.RoundTripper
}

func (t *SSRFValidatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	if req == nil || req.URL == nil {
		return nil, fmt.Errorf("outbound request blocked: request URL is required")
	}
	if t == nil || t.Base == nil {
		return nil, fmt.Errorf("outbound request blocked: base transport is required")
	}
	if err := validateURLForSSRFForOutbound(req.URL.String()); err != nil {
		return nil, fmt.Errorf("outbound request blocked by SSRF policy: %w", err)
	}
	return t.Base.RoundTrip(req)
}

// NewSSRFSafeHTTPClientWithTransport wraps a caller-supplied transport in an
// *http.Client carrying the given timeout and the SSRF-aware redirect policy.
// Pass a transport from NewSSRFSafeTransport (optionally shared across clients)
// to reuse a single connection pool while keeping per-client timeouts.
func NewSSRFSafeHTTPClientWithTransport(
	config SSRFSafeHTTPClientConfig, transport http.RoundTripper,
) *http.Client {
	if transport == nil {
		transport = NewSSRFSafeTransport(config)
	}
	return &http.Client{

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Set Base to a real transport, e.g. &SSRFValidatingRoundTripper{Base: http.DefaultTransport} (or a custom *http.Transport) before using it.
  2. Use the library's constructor (e.g. NewSSRFSafeHTTPClientWithTransport / equivalent NewSSRFValidatingRoundTripper) instead of a raw struct literal so Base is always populated.
  3. Add an init-time check in your setup code: if rt.Base == nil { return errors.New("round tripper not initialized") } to fail at startup rather than at request time.

Example fix

// before
client := &http.Client{Transport: &utils.SSRFValidatingRoundTripper{}}

// after
client := &http.Client{Transport: &utils.SSRFValidatingRoundTripper{Base: http.DefaultTransport}}
Defensive patterns

Strategy: validation

Validate before calling

if rt == nil || rt.Base == nil {
    return fmt.Errorf("SSRFValidatingRoundTripper not initialized: Base transport is nil")
}

Type guard

func isReadySSRFRoundTripper(t *utils.SSRFValidatingRoundTripper) bool {
    return t != nil && t.Base != nil
}

Prevention

When it happens

Trigger: Calling RoundTrip (directly or via http.Transport/Client using this as RoundTripper) on an SSRFValidatingRoundTripper value created as a zero value (&SSRFValidatingRoundTripper{}) or via a constructor path that never set Base. The test TestSSRFValidatingRoundTripperUsesOutboundCache exercises this wrapper and surfaces it when Base is unset.

Common situations: Declaring the round tripper as a struct literal without assigning Base; copying the wrapper by value after construction in a way that drops the field; wiring it into an http.Client before the underlying transport is initialized; refactoring that renamed/removed the field initialization.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/39f5530e77291366. Report an issue: GitHub.