microsoft/typescript-go · critical

api: failed to write response: %v

Error message

api: failed to write response: %v

What it means

SyncConn.handleRequest finished handling an incoming request (success or error) but protocol.WriteResponse/WriteError to the underlying io.ReadWriteCloser failed. A response that cannot be delivered leaves the framing stream unusable, so the code panics to abort the connection instead of continuing.

Source

Thrown at internal/api/conn_sync.go:156

	if c.timing != nil {
		c.timing.record(msg.Method, time.Since(start))
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	var writeErr error
	if err != nil {
		writeErr = c.protocol.WriteError(msg.ID, &jsonrpc.ResponseError{
			Code:    jsonrpc.CodeInternalError,
			Message: err.Error(),
		})
	} else {
		writeErr = c.protocol.WriteResponse(msg.ID, result)
	}

	if writeErr != nil {
		panic(fmt.Sprintf("api: failed to write response: %v", writeErr))
	}
}

// handleNotification processes an incoming notification.
func (c *SyncConn) handleNotification(ctx context.Context, msg *Message) {
	_ = c.handler.HandleNotification(ctx, msg.Method, msg.Params)
}

// Call sends a request to the client and waits for a response.
// This method is safe to call from multiple goroutines - calls are serialized.
func (c *SyncConn) Call(ctx context.Context, method string, params any) (json.Value, error) {
	// Serialize all Call operations. This is critical because:
	// 1. The msgpack protocol uses method names as response IDs
	// 2. The handler code (project internals) may spawn goroutines that call
	//    filesystem callbacks concurrently
	// 3. We need to ensure write/read pairs are atomic
	c.mu.Lock()
	defer c.mu.Unlock()

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Compare the client's timeout with actual server processing time and raise it or make the call async
  2. Check for proxies/load balancers killing long-lived connections
  3. Make the client read responses to EOF before closing after sending requests
  4. Wrap SyncConn.Run with a recover to log and drop the dead connection instead of crashing

Example fix

// before
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) // server needs 2s

// after
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
Defensive patterns

Strategy: try-catch

Try / catch

defer func() {
	if r := recover(); r != nil {
		if strings.Contains(fmt.Sprint(r), "api: failed to write response") {
			log.Printf("client went away mid-request: %v", r)
			return // drop connection, do not crash
		}
		panic(r)
	}
}()

Prevention

When it happens

Trigger: Any request (e.g. getDiagnostics or a project-config load) completes after the client closed its side; the write returns io.EOF, EPIPE, or 'use of closed network connection'. Also fires for the getServerTiming meta-requests when their writes fail.

Common situations: Client timeout shorter than server processing time (request abandoned, socket closed); client crash or restart mid-request; test doubles that close the pipe right after sending a request; stdout transport closed by a spawning process.

Related errors


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