router-for-me/CLIProxyAPI · error

plugin executor http request panic

Error message

plugin executor %s http request panic: %v

What it means

The plugin's HttpRequest implementation panicked; the adapter's deferred recover() converted the panic into an error, nils the response, and fuses the plugin so later calls fail fast with 'unavailable'.

Solutions

  1. Check the recovered panic value in host logs and the Method/URL being proxied
  2. Fix the plugin's HttpRequest to validate AuthID/Headers/StorageJSON before dereferencing and to return errors instead of panicking
  3. Reinstall/reload the fixed plugin to clear the fuse
  4. Add plugin-side tests for empty-auth and unsupported-method requests
Defensive patterns

Strategy: try-catch

Try / catch

resp, err := adapter.HttpRequest(ctx, auth, req)
if err != nil {
    if strings.Contains(err.Error(), "http request panic") {
        return nil, status.Errorf(http.StatusBadGateway, "plugin proxy failed")
    }
    return nil, err
}

Prevention

When it happens

Trigger: Calling HttpRequest where the plugin panics — e.g. it dereferences fields of the ExecutorHTTPRequest struct it expects to be set (Headers map, StorageJSON) or mishandles a request method/URL it does not support.

Common situations: Plugin assumes auth data exists (StorageJSON empty for anonymous requests); plugin built against an older plugin API struct; unusual HTTP method or header shape reaching the plugin.

Related errors


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

Appendix: source

Thrown at internal/pluginhost/adapters_executors.go:799

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

func (a *executorAdapter) HttpRequest(ctx context.Context, auth *coreauth.Auth, req *http.Request) (resp *http.Response, 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())
	}
	if req == nil {
		return nil, fmt.Errorf("plugin executor %s received nil HTTP request", a.Identifier())
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			a.host.fusePlugin(a.pluginID, "Executor.HttpRequest", recovered)
			resp = nil
			err = fmt.Errorf("plugin executor %s http request panic: %v", a.Identifier(), recovered)
		}
	}()
	body, errReadAll := readAndRestoreRequestBody(req)
	if errReadAll != nil {
		return nil, fmt.Errorf("read plugin http request body: %w", errReadAll)
	}
	pluginResp, errHTTPRequest := a.executor.HttpRequest(ctx, pluginapi.ExecutorHTTPRequest{
		AuthID:       authID(auth),
		AuthProvider: authProvider(auth),
		Method:       req.Method,
		URL:          req.URL.String(),
		Headers:      cloneHeader(req.Header),
		Body:         bytes.Clone(body),
		StorageJSON:  storageJSONFromAuth(auth),
		Metadata:     cloneAnyMap(authMetadata(auth)),
		Attributes:   authAttributes(auth),
		HTTPClient:   a.host.newHTTPClient(auth, a.provider),
	})

View on GitHub (pinned to 78f0c4079e)