{"record":{"id":"2df9dcc6a14c0bc4","repo":"router-for-me/CLIProxyAPI","slug":"select-claude-device-id-session-id-is-empty","errorCode":null,"errorMessage":"select Claude device ID: session ID is empty","messagePattern":"select Claude device ID: session ID is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/claude/identity.go","lineNumber":274,"sourceCode":"\t\tseen[deviceID] = struct{}{}\n\t\tdeviceIDs = append(deviceIDs, deviceID)\n\t}\n\n\tif changed {\n\t\tmetadata[ClaudeDeviceIDsMetadataKey] = append([]string(nil), deviceIDs...)\n\t}\n\treturn append([]string(nil), deviceIDs...), changed, nil\n}\n\n// SelectDeviceID returns the credential's sole device ID after validating the conversation session.\nfunc SelectDeviceID(deviceIDs []string, sessionID string) (string, error) {\n\tdeviceIDs = NormalizeDeviceIDPool(deviceIDs)\n\tif len(deviceIDs) != ClaudeDevicePoolSize {\n\t\treturn \"\", fmt.Errorf(\"select Claude device ID: device pool has %d entries, want %d\", len(deviceIDs), ClaudeDevicePoolSize)\n\t}\n\tsessionID = strings.TrimSpace(sessionID)\n\tif sessionID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"select Claude device ID: session ID is empty\")\n\t}\n\treturn deviceIDs[0], nil\n}\n\n// ValidDeviceID reports whether a value matches Claude Code's lowercase 64-hex device format.\nfunc ValidDeviceID(value string) bool {\n\tif len(value) != claudeDeviceIDByteSize*2 || value != strings.ToLower(value) {\n\t\treturn false\n\t}\n\tdecoded, errDecode := hex.DecodeString(value)\n\treturn errDecode == nil && len(decoded) == claudeDeviceIDByteSize\n}\n","sourceCodeStart":256,"sourceCodeEnd":287,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/claude/identity.go#L256-L287","documentation":"SelectDeviceID validates the Claude credential's device-ID pool and the conversation session ID before returning the device ID used for OAuth session binding. After normalizing the pool it trims the session ID and rejects an empty value, because Claude's API requires a non-empty session identifier to pin requests to a device. This is a pure input-validation error: nothing has touched the network yet when it fires.","triggerScenarios":"Calling SelectDeviceID(deviceIDs, sessionID) where sessionID is \"\", consists only of whitespace, or was never populated (e.g. an empty conversation ID field in a request payload). It only fires after the pool itself already has exactly ClaudeDevicePoolSize entries, so the device pool is valid and the session ID is the sole problem.","commonSituations":"Passing a request's session/conversation ID straight from user input without defaulting it; refactoring a caller so the session ID variable is no longer set; testing with a hand-built credential and forgetting the session field; whitespace-padded session IDs copied from config files.","solutions":["Ensure the caller passes a non-empty conversation/session ID before invoking SelectDeviceID (e.g. default it to the request's conversation ID or a generated UUID).","Trim the session ID at the point of intake so whitespace-only values are caught earlier with a clearer message.","If session binding is optional for your flow, skip the SelectDeviceID call when no session exists instead of passing an empty string.","Check upstream request parsing to confirm the session field name still matches after schema changes."],"exampleFix":"// before\ndeviceID, err := claude.SelectDeviceID(pool.DeviceIDs, req.SessionID)\n\n// after\nsessionID := strings.TrimSpace(req.SessionID)\nif sessionID == \"\" {\n    sessionID = req.ConversationID // or generate a UUID for anonymous use\n}\ndeviceID, err := claude.SelectDeviceID(pool.DeviceIDs, sessionID)","handlingStrategy":"validation","validationCode":"sessionID := strings.TrimSpace(req.SessionID)\nif sessionID == \"\" {\n    return fmt.Errorf(\"conversation requires a session ID\")\n}\ndeviceID, err := claude.SelectDeviceID(pool.DeviceIDs, sessionID)","typeGuard":"func hasSessionID(id string) bool { return strings.TrimSpace(id) != \"\" }","tryCatchPattern":null,"preventionTips":["Default empty session IDs to the conversation ID or a generated UUID at intake.","Trim session IDs from user/config input once, at the boundary."],"tags":["claude","auth","validation","device-id"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}