microsoft/typescript-go · critical
api: unexpected response message in sync connection
Error message
api: unexpected response message in sync connection
What it means
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.
Source
Thrown at internal/api/conn_sync.go:78
c.mu.Lock()
msg, err := c.protocol.ReadMessage()
c.mu.Unlock()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if msg.IsRequest() {
c.handleRequest(ctx, msg)
} else if msg.IsNotification() {
c.handleNotification(ctx, msg)
} else {
// Responses are not expected in the main loop - they are read inline by Call().
return errors.New("api: unexpected response message in sync connection")
}
}
}
// handleRequest processes an incoming request.
func (c *SyncConn) handleRequest(ctx context.Context, msg *Message) {
// Intercept the meta-requests for collected server timing before dispatching
// to the handler, so they are answered directly and not themselves recorded.
switch msg.Method {
case string(MethodGetServerTiming):
c.mu.Lock()
writeErr := c.protocol.WriteResponse(msg.ID, serverTimingSnapshot(c.timing))
c.mu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write server timing response: %v", writeErr))
}
return
case string(MethodResetServerTiming):View on GitHub (pinned to 1bcfa18d79)
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
Example fix
// before
var wg sync.WaitGroup
for i := 0; i < 2; i++ {
wg.Add(1)
go func() { defer wg.Done(); conn.Call(ctx, "a", nil) }() // racy reads on SyncConn
}
// after
for i := 0; i < 2; i++ {
conn.Call(ctx, "a", nil) // sequential, matching SyncConn's design
} Defensive patterns
Strategy: try-catch
Try / catch
err := conn.Run(ctx)
if err != nil && strings.Contains(err.Error(), "unexpected response message") { /* concurrency or peer bug: fix call discipline, restart connection */ } Prevention
- 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
When it happens
Trigger: 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.
Common situations: Refactors that call SyncConn.Call from multiple goroutines; a misbehaving peer duplicating responses; feeding a server-side connection a client-style message stream.
Related errors
- Connection not established
- -32603
- api: remote error [%d]: %s
- api: failed to write server timing response: %v
- api: failed to write reset server timing response: %v
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/8a5fc1b81c2837a6.
Report an issue: GitHub.