router-for-me/CLIProxyAPI · error

plugin executor %s stream panic: %v

Error message

plugin executor %s stream panic: %v

What it means

A panic inside the plugin's ExecuteStream implementation was caught by the executor adapter's deferred recover(). The host converts the panic into a normal Go error, nulls the stream result, and trips the plugin's fuse (circuit breaker) via fusePlugin, so subsequent calls to this plugin fail fast with 'plugin executor ... is unavailable' until the fuse is cleared.

Source

Thrown at internal/pluginhost/adapters_executors.go:670

	if errExecute != nil {
		return coreexecutor.Response{}, errExecute
	}
	return coreexecutor.Response{
		Payload:  a.translateExecutorResponse(ctx, prepared, pluginResp.Payload, false, nil),
		Metadata: cloneAnyMap(pluginResp.Metadata),
		Headers:  cloneHeader(pluginResp.Headers),
	}, nil
}

func (a *executorAdapter) ExecuteStream(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (result *coreexecutor.StreamResult, err error) {
	if a == nil || a.executor == nil || a.host.isPluginFused(a.pluginID) || !a.host.pluginIdentityCurrent(a.pluginID, a.path, a.version) {
		return nil, fmt.Errorf("plugin executor %s is unavailable", a.Identifier())
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			a.host.fusePlugin(a.pluginID, "Executor.ExecuteStream", recovered)
			result = nil
			err = fmt.Errorf("plugin executor %s stream panic: %v", a.Identifier(), recovered)
		}
	}()

	prepared, errPrepare := a.prepareExecutorCall(req, opts)
	if errPrepare != nil {
		return nil, errPrepare
	}
	pluginResp, errExecuteStream := a.executor.ExecuteStream(ctx, buildExecutorRequest(a.host, a.provider, auth, prepared.req, prepared.opts))
	if errExecuteStream != nil {
		return nil, errExecuteStream
	}
	return &coreexecutor.StreamResult{
		Headers: cloneHeader(pluginResp.Headers),
		Chunks:  mapExecutorStreamChunks(ctx, a.translateExecutorStreamChunks(ctx, prepared, pluginResp.Chunks)),
	}, nil
}

func (a *executorAdapter) Refresh(ctx context.Context, auth *coreauth.Auth) (refreshed *coreauth.Auth, err error) {

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Check the host logs for the panic value and stack captured before the fuse tripped; reproduce the plugin panic by running the plugin standalone
  2. Fix the panic inside the plugin's ExecuteStream implementation (the %v in the message is the recovered panic value)
  3. Rebuild/reinstall the plugin so the host picks up the fixed binary; reload plugins or restart the proxy to clear the fuse
  4. If the plugin is from a vendor, report the panic value and upgrade to a fixed plugin release
Defensive patterns

Strategy: try-catch

Try / catch

// Go: the adapter already recovers; guard the error path.
stream, err := adapter.ExecuteStream(ctx, auth, req, opts)
if err != nil {
    if strings.Contains(err.Error(), "stream panic") {
        // plugin is now fused; stop retrying and fail over
        log.Errorf("plugin stream panic (fused): %v", err)
        return failoverOrError(err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ExecuteStream on an executorAdapter whose backing plugin (out-of-process or in-process plugin executor) panics during streaming execution — e.g. nil map write, index out of range, or nil pointer dereference inside the plugin's streaming code path.

Common situations: Third-party or custom plugin written in Go that has a bug in its stream handler; plugin built against an older plugin API version whose stream request shape changed; plugin receiving an unexpected model/payload and dereferencing a nil field.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/9a6fe1923bed2e1c. Report an issue: GitHub.