Fix widespread double JSON encoding bug in daemon RPC calls (bd-1048, bd-4ec8)
Multiple CLI commands had a systematic bug where ResolveID responses were
incorrectly converted using string(resp.Data) instead of json.Unmarshal.
Since resp.Data is json.RawMessage (already JSON-encoded), this preserved
the JSON quotes, causing IDs to become "bd-1048" instead of bd-1048.
When re-marshaled for subsequent RPC calls, these became double-quoted
("\"bd-1048\""), causing database lookups to fail.
Bugs fixed:
1. Nil pointer dereference in handleShow - added nil check after GetIssue
2. Double JSON encoding in 12 locations across 4 commands:
- bd show (3 instances in show.go)
- bd dep add/remove/tree (5 instances in dep.go)
- bd label add/remove/list (3 instances in label.go)
- bd reopen (1 instance in reopen.go)
All instances replaced string(resp.Data) with proper json.Unmarshal.
Removed debug logging added during investigation.
Tested: All affected commands now work correctly with daemon mode.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -38,7 +39,10 @@ var depAddCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fromID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &fromID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
|
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
|
||||||
resp, err = daemonClient.ResolveID(resolveArgs)
|
resp, err = daemonClient.ResolveID(resolveArgs)
|
||||||
@@ -46,7 +50,10 @@ var depAddCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
toID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &toID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
|
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||||
@@ -159,7 +166,10 @@ var depRemoveCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fromID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &fromID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
|
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
|
||||||
resp, err = daemonClient.ResolveID(resolveArgs)
|
resp, err = daemonClient.ResolveID(resolveArgs)
|
||||||
@@ -167,7 +177,10 @@ var depRemoveCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
toID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &toID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
|
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||||
@@ -250,7 +263,10 @@ var depTreeCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fullID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &fullID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
fullID, err = utils.ResolvePartialID(ctx, store, args[0])
|
fullID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ var labelAddCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fullID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &fullID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fullID, err = utils.ResolvePartialID(ctx, store, id)
|
fullID, err = utils.ResolvePartialID(ctx, store, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,7 +128,10 @@ var labelRemoveCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fullID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &fullID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fullID, err = utils.ResolvePartialID(ctx, store, id)
|
fullID, err = utils.ResolvePartialID(ctx, store, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -162,7 +168,10 @@ var labelListCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
issueID = string(resp.Data)
|
if err := json.Unmarshal(resp.Data, &issueID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
issueID, err = utils.ResolvePartialID(ctx, store, args[0])
|
issueID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
var resolvedID string
|
||||||
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ var showCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
var resolvedID string
|
||||||
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// In direct mode, resolve via storage
|
// In direct mode, resolve via storage
|
||||||
@@ -369,7 +374,12 @@ var updateCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
var resolvedID string
|
||||||
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
@@ -650,7 +660,12 @@ var closeCmd = &cobra.Command{
|
|||||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
var resolvedID string
|
||||||
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -191,10 +191,8 @@ func (s *Server) checkAndAutoImportIfStale(req *Request) error {
|
|||||||
// If import is already running, skip and let the request proceed (bd-8931)
|
// If import is already running, skip and let the request proceed (bd-8931)
|
||||||
// This prevents blocking RPC requests when import is in progress
|
// This prevents blocking RPC requests when import is in progress
|
||||||
if !s.importInProgress.CompareAndSwap(false, true) {
|
if !s.importInProgress.CompareAndSwap(false, true) {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: auto-import already in progress, skipping (bd-1048)\n")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Debug: acquired import lock, proceeding with auto-import (bd-1048)\n")
|
|
||||||
|
|
||||||
// Track whether we should release the lock via defer
|
// Track whether we should release the lock via defer
|
||||||
// Set to false if we manually release early to avoid double-release bug
|
// Set to false if we manually release early to avoid double-release bug
|
||||||
@@ -215,23 +213,16 @@ func (s *Server) checkAndAutoImportIfStale(req *Request) error {
|
|||||||
s.importInProgress.Store(false)
|
s.importInProgress.Store(false)
|
||||||
shouldDeferRelease = false
|
shouldDeferRelease = false
|
||||||
|
|
||||||
if os.Getenv("BD_DEBUG") != "" {
|
|
||||||
fmt.Fprintf(os.Stderr, "Debug: skipping auto-import, .beads files have uncommitted changes\n")
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: auto-import skipped - .beads files have uncommitted changes. Run 'bd import' manually after committing.\n")
|
fmt.Fprintf(os.Stderr, "Warning: auto-import skipped - .beads files have uncommitted changes. Run 'bd import' manually after committing.\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check staleness after acquiring lock (another goroutine may have imported)
|
// Double-check staleness after acquiring lock (another goroutine may have imported)
|
||||||
fmt.Fprintf(os.Stderr, "Debug: checking staleness after lock acquisition (bd-1048)\n")
|
|
||||||
isStale, err = autoimport.CheckStaleness(ctx, store, dbPath)
|
isStale, err = autoimport.CheckStaleness(ctx, store, dbPath)
|
||||||
if err != nil || !isStale {
|
if err != nil || !isStale {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: staleness check returned: stale=%v err=%v (bd-1048)\n", isStale, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "Debug: daemon detected stale JSONL, auto-importing with timeout... (bd-1048)\n")
|
|
||||||
|
|
||||||
// Create timeout context for import operation (bd-8931, bd-1048)
|
// Create timeout context for import operation (bd-8931, bd-1048)
|
||||||
// This prevents daemon from hanging if import gets stuck
|
// This prevents daemon from hanging if import gets stuck
|
||||||
// Use shorter timeout (5s) to ensure client doesn't timeout waiting for response
|
// Use shorter timeout (5s) to ensure client doesn't timeout waiting for response
|
||||||
|
|||||||
@@ -385,6 +385,12 @@ func (s *Server) handleShow(req *Request) Response {
|
|||||||
Error: fmt.Sprintf("failed to get issue: %v", err),
|
Error: fmt.Sprintf("failed to get issue: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if issue == nil {
|
||||||
|
return Response{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("issue not found: %s", showArgs.ID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Populate labels, dependencies, and dependents
|
// Populate labels, dependencies, and dependents
|
||||||
labels, _ := store.GetLabels(ctx, issue.ID)
|
labels, _ := store.GetLabels(ctx, issue.ID)
|
||||||
|
|||||||
@@ -178,9 +178,7 @@ func (s *Server) handleSignals() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleConnection(conn net.Conn) {
|
func (s *Server) handleConnection(conn net.Conn) {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: handleConnection started (bd-1048)\n")
|
|
||||||
defer func() {
|
defer func() {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: handleConnection closing (bd-1048)\n")
|
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -196,19 +194,15 @@ func (s *Server) handleConnection(conn net.Conn) {
|
|||||||
writer := bufio.NewWriter(conn)
|
writer := bufio.NewWriter(conn)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: waiting for request (bd-1048)\n")
|
|
||||||
// Set read deadline for the next request
|
// Set read deadline for the next request
|
||||||
if err := conn.SetReadDeadline(time.Now().Add(s.requestTimeout)); err != nil {
|
if err := conn.SetReadDeadline(time.Now().Add(s.requestTimeout)); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: SetReadDeadline error: %v (bd-1048)\n", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
line, err := reader.ReadBytes('\n')
|
line, err := reader.ReadBytes('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: ReadBytes error: %v (bd-1048)\n", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Debug: received request line (bd-1048)\n")
|
|
||||||
|
|
||||||
var req Request
|
var req Request
|
||||||
if err := json.Unmarshal(line, &req); err != nil {
|
if err := json.Unmarshal(line, &req); err != nil {
|
||||||
@@ -219,18 +213,14 @@ func (s *Server) handleConnection(conn net.Conn) {
|
|||||||
s.writeResponse(writer, resp)
|
s.writeResponse(writer, resp)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Debug: parsed request operation: %s (bd-1048)\n", req.Operation)
|
|
||||||
|
|
||||||
// Set write deadline for the response
|
// Set write deadline for the response
|
||||||
if err := conn.SetWriteDeadline(time.Now().Add(s.requestTimeout)); err != nil {
|
if err := conn.SetWriteDeadline(time.Now().Add(s.requestTimeout)); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: SetWriteDeadline error: %v (bd-1048)\n", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := s.handleRequest(&req)
|
resp := s.handleRequest(&req)
|
||||||
fmt.Fprintf(os.Stderr, "Debug: handleRequest returned, writing response (bd-1048)\n")
|
|
||||||
s.writeResponse(writer, resp)
|
s.writeResponse(writer, resp)
|
||||||
fmt.Fprintf(os.Stderr, "Debug: response written (bd-1048)\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,8 +103,6 @@ func (s *Server) validateDatabaseBinding(req *Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleRequest(req *Request) Response {
|
func (s *Server) handleRequest(req *Request) Response {
|
||||||
fmt.Fprintf(os.Stderr, "Debug: handleRequest called for operation: %s (bd-1048)\n", req.Operation)
|
|
||||||
|
|
||||||
// Track request timing
|
// Track request timing
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
@@ -112,7 +110,6 @@ func (s *Server) handleRequest(req *Request) Response {
|
|||||||
defer func() {
|
defer func() {
|
||||||
latency := time.Since(start)
|
latency := time.Since(start)
|
||||||
s.metrics.RecordRequest(req.Operation, latency)
|
s.metrics.RecordRequest(req.Operation, latency)
|
||||||
fmt.Fprintf(os.Stderr, "Debug: handleRequest completed for operation: %s in %v (bd-1048)\n", req.Operation, latency)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Validate database binding (skip for health/metrics to allow diagnostics)
|
// Validate database binding (skip for health/metrics to allow diagnostics)
|
||||||
|
|||||||
Reference in New Issue
Block a user