Files
beads/internal/rpc/server_labels_deps_comments.go
vector-sigma 6408ef65f4 Fix #249: Add nil storage checks to prevent RPC daemon crashes (#250)
The daemon RPC server was crashing with a nil pointer dereference when the
global daemon received list, ready, stats, or other storage-dependent RPC
requests. The global daemon is created with nil storage, causing these
operations to panic when they attempted to access storage methods.

This fix adds defensive nil checks at the beginning of all RPC handlers
that require storage access. When storage is unavailable, they now return
a proper JSON error response instead of crashing the daemon.

The error message also informs users that the global daemon is deprecated
and they should use local daemons instead.

Handlers fixed:
- handleCreate, handleUpdate, handleClose
- handleList, handleShow, handleReady, handleStale
- handleResolveID, handleStats, handleEpicStatus
- handleCompact, handleCompactStats
- handleDepAdd (and via handleSimpleStoreOp for all label/dep/comment ops)

Co-authored-by: Test User <test@example.com>
2025-11-07 14:21:14 -08:00

197 lines
5.0 KiB
Go

package rpc
import (
"context"
"encoding/json"
"fmt"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types"
)
func (s *Server) handleDepAdd(req *Request) Response {
var depArgs DepAddArgs
if err := json.Unmarshal(req.Args, &depArgs); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid dep add args: %v", err),
}
}
store := s.storage
if store == nil {
return Response{
Success: false,
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
}
}
dep := &types.Dependency{
IssueID: depArgs.FromID,
DependsOnID: depArgs.ToID,
Type: types.DependencyType(depArgs.DepType),
}
ctx := s.reqCtx(req)
if err := store.AddDependency(ctx, dep, s.reqActor(req)); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to add dependency: %v", err),
}
}
// Emit mutation event for event-driven daemon
s.emitMutation(MutationUpdate, depArgs.FromID)
return Response{Success: true}
}
// Generic handler for simple store operations with standard error handling
func (s *Server) handleSimpleStoreOp(req *Request, argsPtr interface{}, argDesc string,
opFunc func(context.Context, storage.Storage, string) error, issueID string) Response {
if err := json.Unmarshal(req.Args, argsPtr); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid %s args: %v", argDesc, err),
}
}
store := s.storage
if store == nil {
return Response{
Success: false,
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
}
}
ctx := s.reqCtx(req)
if err := opFunc(ctx, store, s.reqActor(req)); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to %s: %v", argDesc, err),
}
}
// Emit mutation event for event-driven daemon
s.emitMutation(MutationUpdate, issueID)
return Response{Success: true}
}
func (s *Server) handleDepRemove(req *Request) Response {
var depArgs DepRemoveArgs
return s.handleSimpleStoreOp(req, &depArgs, "dep remove", func(ctx context.Context, store storage.Storage, actor string) error {
return store.RemoveDependency(ctx, depArgs.FromID, depArgs.ToID, actor)
}, depArgs.FromID)
}
func (s *Server) handleLabelAdd(req *Request) Response {
var labelArgs LabelAddArgs
return s.handleSimpleStoreOp(req, &labelArgs, "label add", func(ctx context.Context, store storage.Storage, actor string) error {
return store.AddLabel(ctx, labelArgs.ID, labelArgs.Label, actor)
}, labelArgs.ID)
}
func (s *Server) handleLabelRemove(req *Request) Response {
var labelArgs LabelRemoveArgs
return s.handleSimpleStoreOp(req, &labelArgs, "label remove", func(ctx context.Context, store storage.Storage, actor string) error {
return store.RemoveLabel(ctx, labelArgs.ID, labelArgs.Label, actor)
}, labelArgs.ID)
}
func (s *Server) handleCommentList(req *Request) Response {
var commentArgs CommentListArgs
if err := json.Unmarshal(req.Args, &commentArgs); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid comment list args: %v", err),
}
}
store := s.storage
ctx := s.reqCtx(req)
comments, err := store.GetIssueComments(ctx, commentArgs.ID)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to list comments: %v", err),
}
}
data, _ := json.Marshal(comments)
return Response{
Success: true,
Data: data,
}
}
func (s *Server) handleCommentAdd(req *Request) Response {
var commentArgs CommentAddArgs
if err := json.Unmarshal(req.Args, &commentArgs); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid comment add args: %v", err),
}
}
store := s.storage
ctx := s.reqCtx(req)
comment, err := store.AddIssueComment(ctx, commentArgs.ID, commentArgs.Author, commentArgs.Text)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to add comment: %v", err),
}
}
// Emit mutation event for event-driven daemon
s.emitMutation(MutationComment, commentArgs.ID)
data, _ := json.Marshal(comment)
return Response{
Success: true,
Data: data,
}
}
func (s *Server) handleBatch(req *Request) Response {
var batchArgs BatchArgs
if err := json.Unmarshal(req.Args, &batchArgs); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid batch args: %v", err),
}
}
results := make([]BatchResult, 0, len(batchArgs.Operations))
for _, op := range batchArgs.Operations {
subReq := &Request{
Operation: op.Operation,
Args: op.Args,
Actor: req.Actor,
RequestID: req.RequestID,
Cwd: req.Cwd, // Pass through context
ClientVersion: req.ClientVersion, // Pass through version for compatibility checks
}
resp := s.handleRequest(subReq)
results = append(results, BatchResult(resp))
if !resp.Success {
break
}
}
batchResp := BatchResponse{Results: results}
data, _ := json.Marshal(batchResp)
return Response{
Success: true,
Data: data,
}
}