Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/nlm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,16 @@ func run() error {
args := flag.Args()[1:]

var opts []batchexecute.Option
if debug {
opts = append(opts, batchexecute.WithDebug(true))
}

for i := 0; i < 3; i++ {
if i > 1 {
fmt.Fprintln(os.Stderr, "nlm: attempting again to obtain login information")
debug = true
// Update opts to include debug when retrying auth
opts = []batchexecute.Option{batchexecute.WithDebug(true)}
}

if err := runCmd(api.New(authToken, cookies, opts...), cmd, args...); err == nil {
Expand Down
104 changes: 60 additions & 44 deletions internal/batchexecute/batchexecute.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,21 +188,33 @@ func (c *Client) Execute(rpcs []RPC) (*Response, error) {
return &responses[0], nil
}

var debug = true
var debug = false

// decodeResponse decodes the batchexecute response
func decodeResponse(raw string) ([]Response, error) {
raw = strings.TrimPrefix(raw, ")]}'")
if raw == "" {
return nil, fmt.Errorf("empty response after trimming prefix")
}
var responses [][]interface{}
var responses interface{}
if err := json.NewDecoder(strings.NewReader(raw)).Decode(&responses); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}

// Convert to [][]interface{} if it's an array
var responseArray [][]interface{}
if respSlice, ok := responses.([]interface{}); ok {
for _, item := range respSlice {
if itemSlice, ok := item.([]interface{}); ok {
responseArray = append(responseArray, itemSlice)
}
}
} else {
return nil, fmt.Errorf("unexpected response format: %T", responses)
}

var result []Response
for _, rpcData := range responses {
for _, rpcData := range responseArray {
if len(rpcData) < 7 {
continue
}
Expand Down Expand Up @@ -293,25 +305,52 @@ func decodeChunkedResponse(raw string) ([]Response, error) {
len(chunk), string(chunk[:min(50, len(chunk))]))
}

// First try to parse as regular JSON
var rpcBatch [][]interface{}
if err := json.Unmarshal(chunk, &rpcBatch); err != nil {
// If that fails, try unescaping the JSON string first
unescaped, err := strconv.Unquote("\"" + string(chunk) + "\"")
if err != nil {
if debug {
fmt.Printf("Failed to unescape chunk: %v\n", err)
// Check if JSON is complete and handle truncated chunks
var testBatch [][]interface{}
if err := json.Unmarshal(chunk, &testBatch); err != nil {
// Try to read more data until we get valid JSON or hit EOF
extraData := make([]byte, 0)
buf := make([]byte, 1024)

for attempts := 0; attempts < 10; attempts++ {
n, err := reader.Read(buf)
if err == io.EOF || err != nil || n == 0 {
break
}
extraData = append(extraData, buf[:n]...)

// Try parsing with extra data
fullChunk := append(chunk, extraData...)
if err := json.Unmarshal(fullChunk, &testBatch); err == nil {
chunk = fullChunk
break
}
return nil, fmt.Errorf("failed to parse chunk: %w", err)
}
if err := json.Unmarshal([]byte(unescaped), &rpcBatch); err != nil {
if debug {
fmt.Printf("Failed to parse unescaped chunk: %v\n", err)

// If we read extra data, find the first complete JSON object
if len(extraData) > 0 {
fullData := append(chunk, extraData...)

// Use JSON decoder to find first complete object boundary
decoder := json.NewDecoder(strings.NewReader(string(fullData)))
var firstJSON [][]interface{}
if err := decoder.Decode(&firstJSON); err == nil {
// Use only the first complete JSON object
consumed := decoder.InputOffset()
chunk = fullData[:consumed]
} else {
// If still can't parse, use all data
chunk = fullData
}
return nil, fmt.Errorf("failed to parse chunk: %w", err)
}
}

// Parse the (now complete) chunk as JSON
var rpcBatch [][]interface{}
if err := json.Unmarshal(chunk, &rpcBatch); err != nil {
return nil, fmt.Errorf("failed to parse chunk: %w", err)
}

// Process each RPC response in the batch
for _, rpcData := range rpcBatch {
if len(rpcData) < 7 {
Expand All @@ -333,37 +372,14 @@ func decodeChunkedResponse(raw string) ([]Response, error) {
ID: id,
}

// Handle data - parse the nested JSON string
// Handle data - use string directly to avoid double escaping
if rpcData[2] != nil {
if dataStr, ok := rpcData[2].(string); ok {
// Try to parse the data string
var data interface{}
if err := json.Unmarshal([]byte(dataStr), &data); err != nil {
// If direct parsing fails, try unescaping first
unescaped, err := strconv.Unquote("\"" + dataStr + "\"")
if err != nil {
if debug {
fmt.Printf("Failed to unescape data: %v\n", err)
}
continue
}
if err := json.Unmarshal([]byte(unescaped), &data); err != nil {
if debug {
fmt.Printf("Failed to parse unescaped data: %v\n", err)
}
continue
}
}
// Re-encode to get properly formatted JSON
rawData, err := json.Marshal(data)
if err != nil {
if debug {
fmt.Printf("Failed to re-encode response data: %v\n", err)
}
continue
}
resp.Data = rawData
resp.Data = json.RawMessage(dataStr)
}
} else {
// Set empty array as default for null data
resp.Data = json.RawMessage("[]")
}

// Handle index
Expand Down