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

  1. Use one goroutine (or serialize with the connection's own lock) for all calls on a SyncConn
  2. Switch to AsyncConn if concurrent calls are genuinely needed
  3. Capture the offending message ID and compare against pending calls to find who sent the request
  4. 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

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


AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16). Data as JSON: /api/errors/8a5fc1b81c2837a6. Report an issue: GitHub.