{"record":{"id":"8a5fc1b81c2837a6","repo":"microsoft/typescript-go","slug":"api-unexpected-response-message-in-sync-connectio","errorCode":null,"errorMessage":"api: unexpected response message in sync connection","messagePattern":"api: unexpected response message in sync connection","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/api/conn_sync.go","lineNumber":78,"sourceCode":"\n\t\tc.mu.Lock()\n\t\tmsg, err := c.protocol.ReadMessage()\n\t\tc.mu.Unlock()\n\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tif msg.IsRequest() {\n\t\t\tc.handleRequest(ctx, msg)\n\t\t} else if msg.IsNotification() {\n\t\t\tc.handleNotification(ctx, msg)\n\t\t} else {\n\t\t\t// Responses are not expected in the main loop - they are read inline by Call().\n\t\t\treturn errors.New(\"api: unexpected response message in sync connection\")\n\t\t}\n\t}\n}\n\n// handleRequest processes an incoming request.\nfunc (c *SyncConn) handleRequest(ctx context.Context, msg *Message) {\n\t// Intercept the meta-requests for collected server timing before dispatching\n\t// to the handler, so they are answered directly and not themselves recorded.\n\tswitch msg.Method {\n\tcase string(MethodGetServerTiming):\n\t\tc.mu.Lock()\n\t\twriteErr := c.protocol.WriteResponse(msg.ID, serverTimingSnapshot(c.timing))\n\t\tc.mu.Unlock()\n\t\tif writeErr != nil {\n\t\t\tpanic(fmt.Sprintf(\"api: failed to write server timing response: %v\", writeErr))\n\t\t}\n\t\treturn\n\tcase string(MethodResetServerTiming):","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/api/conn_sync.go#L60-L96","documentation":"SyncConn's read loop only accepts requests and notifications; responses are consumed inline by Call(). Receiving a response message in the main loop means a response arrived with no (or an already-satisfied) pending call — protocol misuse that aborts the connection loop.","triggerScenarios":"The peer answered a request the sync connection never made, or a second goroutine interleaved reads so a response was left for the main loop; clients reusing a SyncConn concurrently from multiple goroutines.","commonSituations":"Refactors that call SyncConn.Call from multiple goroutines; a misbehaving peer duplicating responses; feeding a server-side connection a client-style message stream.","solutions":["Use one goroutine (or serialize with the connection's own lock) for all calls on a SyncConn","Switch to AsyncConn if concurrent calls are genuinely needed","Capture the offending message ID and compare against pending calls to find who sent the request","File a bug if the peer is this project's own server — responses should only follow requests it received"],"exampleFix":"// before\nvar wg sync.WaitGroup\nfor i := 0; i < 2; i++ {\n    wg.Add(1)\n    go func() { defer wg.Done(); conn.Call(ctx, \"a\", nil) }() // racy reads on SyncConn\n}\n\n// after\nfor i := 0; i < 2; i++ {\n    conn.Call(ctx, \"a\", nil) // sequential, matching SyncConn's design\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"err := conn.Run(ctx)\nif err != nil && strings.Contains(err.Error(), \"unexpected response message\") { /* concurrency or peer bug: fix call discipline, restart connection */ }","preventionTips":["Issue calls on a SyncConn from a single goroutine only","Use AsyncConn when you need concurrent calls","Never send responses to a connection that only makes requests"],"tags":["json-rpc","go","concurrency","sync-connection"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}