<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ImportStatic]]></title><description><![CDATA[ImportStatic]]></description><link>https://importstatic.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac70c7c9656db65b995a340/ac3567b5-fba4-453d-b3f2-98a108fb9306.png</url><title>ImportStatic</title><link>https://importstatic.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 18:53:03 GMT</lastBuildDate><atom:link href="https://importstatic.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What breaks when you move a Go service to anthropic-sdk-go v2]]></title><description><![CDATA[Originally published on ImportStatic. Three changes in the new major version of Anthropic's Go SDK alter what your service sends without touching your…]]></description><link>https://importstatic.hashnode.dev/what-breaks-when-you-move-a-go-service-to-anthropic-sdk-go-v2</link><guid isPermaLink="true">https://importstatic.hashnode.dev/what-breaks-when-you-move-a-go-service-to-anthropic-sdk-go-v2</guid><dc:creator><![CDATA[ImportStatic]]></dc:creator><pubDate>Wed, 04 Nov 2026 00:19:03 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://importstatic.com/ai/anthropic-sdk-go-v2-migration"><em>ImportStatic</em></a><em>.</em></p>
<p>Three changes in the new major version of Anthropic's Go SDK alter what your service sends without touching your request code. Version 2.0.0 came out on October 21 together with Claude API version <code>2026-10-21</code>. It sends <code>anthropic-version: 2026-10-21</code>, which rejects <code>temperature</code>, <code>top_p</code> and <code>top_k</code>. It moves your API key from <code>X-Api-Key</code> to <code>Authorization: Bearer</code>. And a client that is given two credentials now fails every request instead of sending both.</p>
<p>v2 lives at a new module path, so none of this applies until you change your imports. <code>go get -u ./...</code> leaves a service on v1.84.0, the last v1 release, which still sends <code>2023-06-01</code>. As of October 29 the <a href="https://platform.claude.com/docs/en/api/versioning">versions page</a> lists no retirement date for <code>2023-06-01</code>, so you can choose when to move. The examples below run one small program against v1.84.0 and v2.4.0 (the current release). All requests went to an <code>httptest</code> server that returns a canned Messages response, so nothing was sent to Anthropic and no claim below is about how the real API answered.</p>
<h2>Compile errors after changing the import path</h2>
<p>The <a href="https://platform.claude.com/docs/en/api/migration-2026-10-21">migration guide</a> says to change every import of <code>github.com/anthropics/anthropic-sdk-go</code>, including subpackages such as <code>option</code>, to start with <code>github.com/anthropics/anthropic-sdk-go/v2</code>, then run <code>go get github.com/anthropics/anthropic-sdk-go/v2</code>. We did that to a v1 program that built its request like this:</p>
<pre><code class="language-go">params := anthropic.MessageNewParams{
	Model:       "claude-opus-5-5",
	MaxTokens:   64,
	Temperature: anthropic.Float(0.2),
	Messages: []anthropic.MessageParam{{
		Role:    anthropic.MessageParamRoleUser,
		Content: []anthropic.ContentBlockParamUnion{anthropic.NewTextBlock("Hello")},
	}},
}
</code></pre>
<p>With only the import lines changed, Go 1.27.3 stops at four errors:</p>
<pre><code class="language-text">./main.go:48:3: unknown field Temperature in struct literal of type anthropic.MessageNewParams
./main.go:50:4: unknown field Role in struct literal of type anthropic.MessageParam
./main.go:50:23: undefined: anthropic.MessageParamRoleUser
./main.go:51:4: unknown field Content in struct literal of type anthropic.MessageParam
</code></pre>
<p>The first error comes from the API change. <code>2026-10-21</code> returns a 400 for any request that sends <code>temperature</code>, <code>top_p</code> or <code>top_k</code>, even at the default value, and the SDK removed the fields so you can't send them by accident. The guide notes that Claude 4.7 and later models already accept only the defaults, so on those models deleting the field changes nothing. On an older model that accepts other values, such as Sonnet 4.6, a request that lowered <code>temperature</code> loses that effect, and no other setting replaces it.</p>
<p>The other three come from <code>MessageParam</code>, which is now a union with one type per role: <code>OfUser</code>, <code>OfAssistant</code> and <code>OfSystem</code>. <code>NewUserMessage</code>, <code>NewAssistantMessage</code> and <code>Message.ToParam</code> work as before, so the smallest fix is to stop writing the struct by hand:</p>
<pre><code class="language-go">params := anthropic.MessageNewParams{
	Model:     "claude-opus-5-5",
	MaxTokens: 64,
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")),
	},
}
</code></pre>
<p>Where you do need a literal, name the role's type and leave <code>Role</code> out, because each type marshals its own. Code that read <code>message.Role</code> checks which pointer is set instead, or calls <code>GetRole()</code>, which returns a <code>*string</code>:</p>
<pre><code class="language-go">history := []anthropic.MessageParam{
	params.Messages[0],
	{OfAssistant: &amp;anthropic.AssistantMessageParam{
		Content: []anthropic.ContentBlockParamUnion{anthropic.NewTextBlock("Hi")},
	}},
}
for _, m := range history {
	fmt.Printf("  role=%s user=%v assistant=%v\n", *m.GetRole(), m.OfUser != nil, m.OfAssistant != nil)
}
</code></pre>
<pre><code class="language-text">  role=user user=true assistant=false
  role=assistant user=false assistant=true
</code></pre>
<p>Because the union holds pointers, copying a <code>MessageParam</code> no longer copies the message, and the compiler won't flag it. Code that trims or rewrites a copy of the history, which is common in agent loops that shorten old tool results, now edits the original:</p>
<pre><code class="language-go">copied := history[1]
copied.OfAssistant.Content = nil
fmt.Printf("  after clearing the copy, original has %d blocks\n", len(history[1].OfAssistant.Content))
</code></pre>
<pre><code class="language-text">  after clearing the copy, original has 0 blocks
</code></pre>
<p>In v1, setting <code>copied.Content</code> changed only the copy. If you keep a canonical history and derive trimmed requests from it, allocate a new <code>AssistantMessageParam</code> for anything you change.</p>
<p>The JSON body is unchanged. Both versions sent <code>{"content":[{"text":"Hello","type":"text"}],"role":"user"}</code> for the message; the only difference in our request body was the missing <code>"temperature":0.2</code>.</p>
<h2>The API key moved to the Authorization header</h2>
<p>This change produces no compile errors. Our stub server received these headers for a plain request from each version:</p>
<pre><code class="language-text">v1.84.0
  anthropic-version: 2023-06-01
  x-api-key:         &lt;key&gt;
  authorization:     -
v2.4.0
  anthropic-version: 2026-10-21
  x-api-key:         -
  authorization:     Bearer &lt;key&gt;
</code></pre>
<p>The Claude API accepts a key in either header, so calls straight to <code>api.anthropic.com</code> keep working. What breaks is anything in between that looks for <code>X-Api-Key</code>: a corporate gateway, a logging proxy, a redaction rule in your log pipeline, a test stub that asserts on the header. If you run something like the <a href="https://importstatic.com/ai/llm-logging-proxy-go">logging proxy from our earlier article</a>, check which header it reads and which it scrubs from logs before you upgrade its clients.</p>
<p>If the proxy can't change yet, the guide's workaround sets <code>X-Api-Key</code> yourself and removes the default <code>Authorization</code> header. Our stub received this:</p>
<pre><code class="language-go">proxied := anthropic.NewClient(
	option.WithBaseURL(srv.URL),
	option.WithHeader("X-Api-Key", "sk-test-key"),
	option.WithHeaderDel("Authorization"),
)
</code></pre>
<pre><code class="language-text">  x-api-key:         &lt;key&gt;
  authorization:     -
</code></pre>
<p>Code that used <code>option.WithHeaderDel("X-Api-Key")</code> to keep the key off one request, usually so an auth token would be the only credential, is affected too. On v1.84.0 that request arrived with no key. On v2.4.0 it arrives with the key anyway, because the key is no longer in that header:</p>
<pre><code class="language-text">2. WithHeaderDel("X-Api-Key") on the request
  anthropic-version: 2026-10-21
  x-api-key:         -
  authorization:     Bearer &lt;key&gt;
</code></pre>
<p>There is no error or warning, so search your code for <code>WithHeaderDel("X-Api-Key")</code> and <code>WithHeader("X-Api-Key"</code> before upgrading. The other old pattern, <code>WithHeader("X-Api-Key", otherKey)</code>, now sends both keys. According to the guide, <code>WithHeaderDel("Authorization")</code> removes the key from a request, and <code>option.WithAPIKey(otherKey)</code> or <code>option.WithAuthToken(token)</code> on a request replaces the client's credential for that request.</p>
<h2>Clients with two credentials</h2>
<p>v1 sent both credentials when a client had an API key and an auth token, each in its own header. Our v1.84.0 client built with <code>WithAPIKey</code> and <code>WithAuthToken</code> delivered <code>x-api-key: &lt;key&gt;</code> and <code>authorization: Bearer gateway-token</code>. The same client on v2.4.0 never reached the server:</p>
<pre><code class="language-text">3. client with an API key and an auth token
  error: option: pass at most one of WithAPIKey, WithAuthToken, WithConfig, WithConfigQuiet, WithProfile and WithFederationTokenProvider; got WithAPIKey and WithAuthToken
</code></pre>
<p><code>anthropic.NewClient</code> returns no error, so this shows up at the first call rather than at startup. <code>NewClient</code> already applies <code>DefaultClientOptions()</code>, so passing its result again means a key found in <code>ANTHROPIC_API_KEY</code> counts as a second credential next to the one you pass. A gateway setup that sent a gateway token plus an API key needs rewriting. The guide's version keeps <code>WithAuthToken</code>, sets <code>X-Api-Key</code> with <code>option.WithHeader</code>, and has the gateway strip its <code>Authorization</code> header before forwarding.</p>
<p>If you pass no credential, the order is the same as v1: <code>ANTHROPIC_API_KEY</code>, then <code>ANTHROPIC_AUTH_TOKEN</code>, then <code>ANTHROPIC_PROFILE</code>, workload identity federation and the active profile on disk.</p>
<h2>Keeping one request on 2023-06-01</h2>
<p><a href="https://github.com/anthropics/anthropic-sdk-go/issues/483">Issue 483</a>, opened on October 27 and still unanswered when we checked, asks whether v2 drops support for <code>2023-06-01</code>, since <code>temperature</code> is gone from the params. The guide's answer for the API is that a request relying on a non-default <code>temperature</code> can stay on <code>2023-06-01</code>. In the SDK, both overrides are per-request options. We sent this from v2.4.0:</p>
<pre><code class="language-go">client.Messages.New(ctx, params,
	option.WithHeader("anthropic-version", "2023-06-01"),
	option.WithJSONSet("temperature", 0.2),
)
</code></pre>
<pre><code class="language-text">6. one request kept on 2023-06-01 with temperature
  anthropic-version: 2023-06-01
  x-api-key:         -
  authorization:     Bearer &lt;key&gt;
  body: {"max_tokens":64,"messages":[{"content":[{"text":"Hello","type":"text"}],"role":"user"}],"model":"claude-opus-5-5","temperature":0.2}
</code></pre>
<p>The header override replaced the default version and the field reached the body. We couldn't send it to the real API from here, and the SDK makes no promise that its v2 types fit the older version. Use it for the few requests on older models that need it; if a whole service needs <code>2023-06-01</code>, stay on v1 for now.</p>
<h2>Other breaking changes</h2>
<p>These are smaller, and the compiler finds most of them:</p>
<ul>
<li>Go 1.25 or later is required.</li>
<li>The Text Completions API is gone from the SDK.</li>
<li><code>ModelListParams</code> and <code>ModelGetParams</code> lost <code>Betas</code>. Call <code>client.Beta.Models.List</code> with <code>BetaModelListParams</code> to send beta headers.</li>
<li><code>vertex.DefaultVersion</code> and <code>bedrock.DefaultVersion</code> are removed. On Amazon Bedrock, <code>2026-10-21</code> works only with Claude 4.7 and later models, so services calling older Bedrock models should stay on v1.</li>
<li>One-value enum types in the Managed Agents beta, such as <code>BetaManagedAgentsTextBlockTypeText</code>, became constants. Delete the <code>Type</code> field in requests and compare responses with the string.</li>
<li><code>ClearAt</code> and <code>OutputConfig</code> moved from <code>BetaMessageParam</code> to <code>BetaSystemMessageParam</code>.</li>
<li><code>option.WithConfigQuiet</code> now does what <code>WithConfig</code> does and is deprecated.</li>
<li>v2 is licensed under Apache 2.0; v1 stays MIT. Update any license inventory you keep.</li>
</ul>
<p>On the API side, two more request shapes return 400 on <code>2026-10-21</code> and compile without errors: a <code>document</code> block placed directly in an <code>assistant</code> message, and a <code>text</code> block with a non-empty <code>citations</code> array in a <code>user</code> message. Both usually come from agent code that replays earlier turns, so search the code that builds history as well as the code that builds new requests.</p>
<p>For the upgrade itself: change the imports, fix what the compiler reports, search for <code>X-Api-Key</code> in code, proxies and log filters, check that each client gets exactly one credential, and only then deploy. If your service also exports traces, the <a href="https://importstatic.com/ai/anthropic-sdk-go-opentelemetry-baggage">OpenTelemetry behaviour added in v1.81</a> carries over to v2, so the baggage check from that article still applies.</p>
<hr />
<p><em>Originally published on</em> <a href="https://importstatic.com/ai/anthropic-sdk-go-v2-migration"><em>ImportStatic</em></a><em>. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.</em></p>
]]></content:encoded></item><item><title><![CDATA["all goroutines are asleep - deadlock!" in Go: six causes and their fixes]]></title><description><![CDATA[Originally published on ImportStatic. To find where a Go program is stuck, read the goroutine dump under fatal error: all goroutines are asleep…]]></description><link>https://importstatic.hashnode.dev/all-goroutines-are-asleep-deadlock-in-go-six-causes-and-their-fixes</link><guid isPermaLink="true">https://importstatic.hashnode.dev/all-goroutines-are-asleep-deadlock-in-go-six-causes-and-their-fixes</guid><category><![CDATA[Go Language]]></category><category><![CDATA[golang]]></category><dc:creator><![CDATA[ImportStatic]]></dc:creator><pubDate>Sun, 01 Nov 2026 15:50:48 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://importstatic.com/go/go-all-goroutines-asleep-deadlock"><em>ImportStatic</em></a><em>.</em></p>
<p>To find where a Go program is stuck, read the goroutine dump under <code>fatal error: all goroutines are asleep - deadlock!</code>. The word in square brackets after <code>goroutine 1</code> is what that goroutine was waiting for, and the last <code>main.</code> frame is the line that blocked. The runtime prints the error and exits with status 2 when no thread is running Go code, every goroutine is blocked and no timer is pending, because at that point nothing can wake the program up.</p>
<p>The six causes below were reproduced with Go 1.27.3, and each fix was compiled and run. In most servers and under <code>go test</code>, the same mistakes hang forever instead of crashing, because a pending timer or a goroutine waiting on the network keeps the runtime from declaring a deadlock.</p>
<h2>States in the goroutine dump</h2>
<table>
<thead>
<tr>
<th>State in the dump</th>
<th>What the goroutine is waiting for</th>
<th>Usual cause</th>
</tr>
</thead>
<tbody><tr>
<td><code>[chan send]</code></td>
<td>a receiver</td>
<td>unbuffered send with nobody receiving</td>
</tr>
<tr>
<td><code>[chan receive]</code></td>
<td>a value or a <code>close</code></td>
<td><code>range</code> over a channel nobody closes</td>
</tr>
<tr>
<td><code>[chan receive (nil chan)]</code>, <code>[chan send (nil chan)]</code></td>
<td>nothing, ever</td>
<td>a channel that was never made</td>
</tr>
<tr>
<td><code>[select]</code></td>
<td>any of its cases</td>
<td>every sender returned without sending</td>
</tr>
<tr>
<td><code>[sync.WaitGroup.Wait]</code></td>
<td>the counter to reach zero</td>
<td>a missing <code>Done</code></td>
</tr>
<tr>
<td><code>[sync.Mutex.Lock]</code></td>
<td>the mutex</td>
<td>locking a mutex the same goroutine already holds</td>
</tr>
</tbody></table>
<p><code>go vet</code> reported nothing on any of the six programs.</p>
<h2>Sending on an unbuffered channel with no receiver</h2>
<pre><code class="language-go">func main() {
	ch := make(chan int)
	ch &lt;- 1 // no receiver is running yet, so this send blocks forever
	fmt.Println(&lt;-ch)
}
</code></pre>
<pre><code class="language-text">fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/unbuffered/main.go:7 +0x38
</code></pre>
<p>The <a href="https://go.dev/ref/spec">spec</a> says a send on an unbuffered channel can proceed only "if a receiver is ready". The receive is on the next line of the same goroutine, so it never runs. Either send from another goroutine or give the channel room for the value:</p>
<pre><code class="language-go">	ch := make(chan int)
	go func() { ch &lt;- 1 }() // the send runs in its own goroutine
	fmt.Println(&lt;-ch)

	buf := make(chan int, 1) // or give the channel room for one value
	buf &lt;- 2
	fmt.Println(&lt;-buf)
</code></pre>
<p>With a buffer of one, a second send before anyone reads blocks again, so a buffer is a fix only when you know the number of values in advance.</p>
<h2>A WaitGroup that never reaches zero</h2>
<pre><code class="language-go">	var wg sync.WaitGroup
	for i := range 3 {
		wg.Add(1)
		go func() {
			if i == 1 {
				return // early return skips Done
			}
			fmt.Println("worker", i)
			wg.Done()
		}()
	}
	wg.Wait()
</code></pre>
<pre><code class="language-text">worker 2
worker 0
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.WaitGroup.Wait]:
sync.runtime_SemacquireWaitGroup(0x1042b0210?, 0x30?)
	/Users/pike6/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.27.3.darwin-arm64/src/sync/waitgroup.go:114 +0x38
sync.(*WaitGroup).Wait(0xb2933396020)
	/Users/pike6/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.27.3.darwin-arm64/src/sync/waitgroup.go:206 +0xa4
main.main()
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/waitgroup/main.go:20 +0xac
</code></pre>
<p>The two workers that printed have exited, so the dump shows only <code>main</code> waiting. The goroutine that returned early is gone too, which is why the trace can't point at it; count your <code>Add</code> and <code>Done</code> calls on every return path. <code>defer wg.Done()</code> at the top of the goroutine fixes it, and since Go 1.25 <a href="https://pkg.go.dev/sync#WaitGroup.Go"><code>WaitGroup.Go</code></a> does the <code>Add</code> and the deferred <code>Done</code> for you:</p>
<pre><code class="language-go">	for i := range 3 {
		wg.Go(func() { // Add(1) before the goroutine starts, Done when it returns
			if i == 1 {
				return
			}
			fmt.Println("worker", i)
		})
	}
	wg.Wait()
</code></pre>
<p>This printed both workers and then <code>all done</code>.</p>
<h2>Ranging over a channel nobody closes</h2>
<pre><code class="language-go">	ch := make(chan int)
	go func() {
		for i := range 3 {
			ch &lt;- i
		}
		// missing close(ch)
	}()
	for v := range ch {
		fmt.Println(v)
	}
</code></pre>
<pre><code class="language-text">0
1
2
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/rangeopen/main.go:13 +0xc0
</code></pre>
<p>All three values arrive, then the <code>for</code> line waits for a fourth value or a close. A <code>range</code> over a channel only ends when the channel is closed. The goroutine that sends should close it, with <code>defer close(ch)</code> as its first line, so the loop ends after the last value. When several goroutines send on one channel, close it from a separate goroutine after a <code>WaitGroup</code> says all senders are done; closing it from a sender that finishes early makes the others panic on a closed channel.</p>
<h2>A select where no case can ever be ready</h2>
<pre><code class="language-go">	results := make(chan string)
	errs := make(chan error)
	go func() {
		fmt.Println("worker finished without sending")
	}()
	select {
	case r := &lt;-results:
		fmt.Println(r)
	case err := &lt;-errs:
		fmt.Println(err)
	}
</code></pre>
<pre><code class="language-text">worker finished without sending
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/selectnone/main.go:11 +0x8c
</code></pre>
<p>In real code the worker usually has a path that returns early, often an error path, without sending on either channel. Make every return path send on exactly one of them:</p>
<pre><code class="language-go">	go func() {
		r, err := work()
		if err != nil {
			errs &lt;- err // every return path sends on one of the channels
			return
		}
		results &lt;- r
	}()
</code></pre>
<p>An empty <code>select {}</code> blocks forever and, with nothing else running, dies with <code>goroutine 1 [select (no cases)]</code>. Servers put it at the end of <code>main</code> on purpose; there a goroutine blocked on a listener keeps the program alive, for the reason given under "When the same mistake hangs instead".</p>
<h2>A channel that was never made</h2>
<pre><code class="language-go">type Server struct {
	done chan struct{} // never initialised
}

func main() {
	s := &amp;Server{}
	&lt;-s.done // receive from a nil channel blocks forever
	fmt.Println("stopped")
}
</code></pre>
<pre><code class="language-text">fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:
main.main()
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/nilchan/main.go:11 +0x24
</code></pre>
<p>The zero value of a channel field is <code>nil</code>, and the spec says "Receiving from a <code>nil</code> channel blocks forever"; sending does too. The <code>(nil chan)</code> in the bracket identifies this case. Make the channel in a constructor (<code>&amp;Server{done: make(chan struct{})}</code>) and don't build the struct with a literal elsewhere.</p>
<h2>Locking a mutex you already hold</h2>
<pre><code class="language-go">func (c *Counter) Get(k string) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.n[k]
}

func (c *Counter) Incr(k string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n[k] = c.Get(k) + 1 // Get locks c.mu again
}
</code></pre>
<pre><code class="language-text">fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.Mutex.Lock]:
internal/sync.runtime_SemacquireMutex(0x1044c9778?, 0xc0?, 0x10f2b9c04db8?)
	/Users/pike6/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.27.3.darwin-arm64/src/runtime/sema.go:95 +0x28
[... three frames inside internal/sync and sync ...]
main.(*Counter).Get(0x10f2b9c24020, {0x10440fc20, 0x1})
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/mutex/main.go:14 +0x84
main.(*Counter).Incr(0x10f2b9c24020, {0x10440fc20, 0x1})
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/mutex/main.go:22 +0xac
main.main()
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/mutex/main.go:27 +0x70
</code></pre>
<p><code>sync.Mutex</code> is not reentrant: <a href="https://pkg.go.dev/sync#Mutex.Lock"><code>Lock</code></a> blocks until the mutex is free, even when the caller is the goroutine that holds it. Two frames from the same type, an outer one holding the lock and an inner one waiting for it, are the pattern to look for. The usual fix is an unexported helper that assumes the lock is held, called by both exported methods:</p>
<pre><code class="language-go">// get expects c.mu to be held by the caller.
func (c *Counter) get(k string) int { return c.n[k] }

func (c *Counter) Incr(k string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n[k] = c.get(k) + 1
}
</code></pre>
<p><code>RWMutex</code> has a variant of this that we didn't reproduce: its documentation prohibits recursive read locking, because a second <code>RLock</code> waits behind any writer that queued up after the first one.</p>
<h2>When the same mistake hangs instead</h2>
<p>Adding this line at the top of <code>main</code> in the unbuffered-send program stops the error:</p>
<pre><code class="language-go">	time.AfterFunc(time.Hour, func() {})
</code></pre>
<p>The program hangs instead. Our driver killed it after three seconds; left alone it would sit there for an hour. We got the same hang with a goroutine blocked in <code>ln.Accept()</code> on a TCP listener, and with one blocked reading from stdin.</p>
<p>The check lives in <code>checkdead</code> in <a href="https://github.com/golang/go/blob/go1.27.3/src/runtime/proc.go">runtime/proc.go</a>. It runs only when the number of threads running Go code drops to zero, then checks that every user goroutine is waiting, and then, before printing the error, returns early if any P has a timer in its heap. A pending timer means time could still wake something, so the runtime can't call it a deadlock. A goroutine waiting on the network poller keeps a thread parked inside <code>netpoll</code> in the scheduler's <code>findRunnable</code> instead of idle, and a blocking read on stdin keeps a thread in a system call. Either way the count of running threads never reaches zero.</p>
<p>That limits the error to small programs. A server has a listener, a ticker, a connection pool or an HTTP client with timeouts, and a deadlocked request handler there just stops responding.</p>
<p>Tests hit the timer rule too. <code>go test</code> arms an alarm for its <code>-timeout</code> flag (10 minutes by default), so a deadlocked test waits out the timeout and reports that instead:</p>
<pre><code class="language-text">panic: test timed out after 3s
	running tests:
		TestSend (3s)
</code></pre>
<p>With <code>-timeout 0</code> the same test failed in 65 ms with <code>fatal error: all goroutines are asleep - deadlock!</code>, followed by a dump that included <code>goroutine 19 [chan send]</code> at the test's line. Setting a short <code>-timeout</code> while you debug gets you the same dump without the wait, since the timeout panic prints every goroutine as well.</p>
<h2>Finding the line when nothing crashes</h2>
<p>Send the hung process <code>SIGQUIT</code> (Ctrl+\ in the terminal it runs in, or <code>kill -QUIT &lt;pid&gt;</code>). The <a href="https://pkg.go.dev/os/signal">os/signal documentation</a> says a Go program then exits with a stack dump. For the timer program the dump starts with runtime internals, and further down holds the same frame the fatal error would have shown:</p>
<pre><code class="language-text">goroutine 1 gp=0x6b40533aa1e0 m=nil [chan send]:
[... runtime.gopark and runtime.chansend frames ...]
main.main()
	/Users/pike6/work/project/website/importstatic/.work/go-deadlock/cases/timer/main.go:13 +0x50 fp=0x6b4053422ea0 sp=0x6b4053422e40 pc=0x100a077e0
</code></pre>
<p>In a server you'd rather not kill, <code>net/http/pprof</code> serves <code>/debug/pprof/goroutine?debug=2</code>, which its source describes as all goroutine stacks "in the same format as an unrecovered panic", and the process keeps running. If only some goroutines are stuck and the rest of the program is fine, it's a goroutine leak rather than a deadlock, and Go 1.27's <a href="https://importstatic.com/go/go-goroutine-leak-profile">goroutineleak profile</a> lists the ones that can never run again.</p>
<p>For tests of concurrent code, <a href="https://importstatic.com/go/go-synctest-testing-concurrent-code"><code>testing/synctest</code></a> gets around the timer rule. Its bubble has a fake clock, so pending timers don't count as a way out. We wrapped the same test, one-hour <code>AfterFunc</code> included, in <code>synctest.Test</code>, and it failed in 0.00s with <code>panic: deadlock: all goroutines in bubble are blocked</code>, the blocked send listed as <code>[chan send (durable), synctest bubble 1]</code>.</p>
<hr />
<p><em>Originally published on</em> <a href="https://importstatic.com/go/go-all-goroutines-asleep-deadlock"><em>ImportStatic</em></a><em>. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.</em></p>
]]></content:encoded></item><item><title><![CDATA[Does the Brotli RCE (CVE-2026-111497) reach Go and Rust servers?]]></title><description><![CDATA[Originally published on ImportStatic. The remote code execution in CVE-2026-111497 is in Google's C Brotli decoder, which most Go and Rust services never…]]></description><link>https://importstatic.hashnode.dev/does-the-brotli-rce-cve-2026-111497-reach-go-and-rust-servers</link><guid isPermaLink="true">https://importstatic.hashnode.dev/does-the-brotli-rce-cve-2026-111497-reach-go-and-rust-servers</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[ImportStatic]]></dc:creator><pubDate>Sat, 31 Oct 2026 03:20:59 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://importstatic.com/go/brotli-cve-2026-111497-go-rust"><em>ImportStatic</em></a><em>.</em></p>
<p>The remote code execution in CVE-2026-111497 is in Google's C Brotli decoder, which most Go and Rust services never load. Two cases are worth checking anyway. A Go or Rust binary can link the C library through cgo or a <code>-sys</code> crate. And two popular frameworks, Fiber v2 in Go and actix-web in Rust, decompress <code>Content-Encoding: br</code> request bodies before your handler runs, with no configuration.</p>
<p>The advisory went public with the Node.js security release on October 21, 2026, and this article reflects what was published as of October 25, 2026. A <a href="https://news.ycombinator.com/item?id=50219177">Hacker News discussion of the follow-up write-up</a> covered Node, Python and .NET and argued about how often servers decode compressed request bodies at all; it didn't get to Go or Rust.</p>
<h2>Affected versions and ports</h2>
<p>The <a href="https://github.com/advisories/GHSA-9v8h-qqgm-4m35">GitHub advisory GHSA-9v8h-qqgm-4m35</a> puts the bug in <code>c/dec/decode.c</code> of Google Brotli 1.0.3 through 1.2.0, with fixes in 1.0.10, 1.1.1 and 1.2.1. GitHub scores it 9.2 (CVSS 4.0); the <a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases">Node.js release notes</a> rate it High and describe the cause: an invalid block switch was reported as "needs more input" instead of a format error, so the streaming decoder kept writing into an eight-byte internal buffer.</p>
<p>For ports to other languages, the <a href="https://broccoli2shell.com/">disclosure site</a> says code execution is only possible in the C decoder. It says the Go port <code>github.com/andybalholm/brotli</code> had the same logic error but would only have crashed, fixed on October 23, and that Rust's <code>brotli-decompressor</code> catches the condition and returns an error. That gives three groups to look for:</p>
<table>
<thead>
<tr>
<th>What your binary contains</th>
<th>Exposure described by the sources</th>
<th>Action</th>
</tr>
</thead>
<tbody><tr>
<td>C Brotli 1.0.3 to 1.2.0 (cgo, <code>-sys</code> crate, system <code>libbrotli</code>)</td>
<td>out-of-bounds write, possible code execution</td>
<td>update the library or the crate that bundles it</td>
</tr>
<tr>
<td><code>andybalholm/brotli</code> before v1.2.7</td>
<td>same logic error, crash only</td>
<td><code>go get github.com/andybalholm/brotli@v1.2.7</code></td>
</tr>
<tr>
<td>Rust <code>brotli</code> / <code>brotli-decompressor</code></td>
<td>returns an error</td>
<td>nothing for this CVE</td>
</tr>
</tbody></table>
<p>The andybalholm fix is <a href="https://github.com/andybalholm/brotli/commit/33c2a9884269798192b5ecf171288f98939dbe42">commit 33c2a98</a>, tagged v1.2.7. Its message says it ports upstream commit <code>0ed4268a</code>, one of the four C fixes the advisory lists: the block-switch functions in <code>decode.go</code> now return a status, and an invalid switch becomes a <code>BLOCK_SWITCH</code> format error.</p>
<h2>Go: net/http, Echo and Fiber</h2>
<p>The standard library doesn't decode Brotli. We sent a valid Brotli body with <code>Content-Encoding: br</code> to an <code>httptest</code> server and fetched a Brotli response with the default client, on Go 1.27.3:</p>
<pre><code class="language-text">server: Content-Encoding="br", body 28 bytes, equals compressed: true
client sent Accept-Encoding="gzip"
client: Content-Encoding="br", Uncompressed=false, body equals compressed: true
</code></pre>
<p>The handler got the 28 compressed bytes, and the <code>Transport</code> only advertises and transparently decodes gzip. A plain <code>net/http</code> service decodes Brotli only where your code, or a middleware you added, calls a decoder. Echo's <code>middleware.Decompress</code> doesn't count either: in v4.16.2 it only acts when <code>Content-Encoding</code> is <code>gzip</code>.</p>
<p>Fiber v2 is different. In <a href="https://github.com/gofiber/fiber/blob/v2.52.16/ctx.go">Fiber's <code>ctx.go</code></a>, <code>Ctx.Body()</code> reads <code>Content-Encoding</code> and calls fasthttp's <code>BodyGunzip</code>, <code>BodyUnbrotli</code> or <code>BodyInflate</code> for each listed coding; only <code>BodyRaw()</code> returns the bytes as sent. The test handler:</p>
<pre><code class="language-go">app.Post("/", func(c *fiber.Ctx) error {
	fmt.Printf("BodyRaw(): %d bytes\n", len(c.BodyRaw()))
	fmt.Printf("Body():    %d bytes %s\n", len(c.Body()), c.Body())
	return nil
})
</code></pre>
<pre><code class="language-text">BodyRaw(): 28 bytes
Body():    33 bytes {"msg":"hello hello hello hello"}
</code></pre>
<p>Which decoder that is depends on the fasthttp version. <a href="https://github.com/valyala/fasthttp/releases/tag/v1.74.0">fasthttp v1.74.0</a>, released September 7, 2026, replaced andybalholm/brotli with <code>github.com/molecule-man/go-brrr</code>, a separate pure-Go implementation. But Fiber v2.52.16's own <code>go.mod</code> only requires fasthttp v1.51.0, so a new project gets the old pair:</p>
<pre><code class="language-text">$ go get github.com/gofiber/fiber/v2@v2.52.16
$ go list -m github.com/valyala/fasthttp github.com/andybalholm/brotli
github.com/valyala/fasthttp v1.51.0
github.com/andybalholm/brotli v1.1.0
</code></pre>
<p>Don't count on govulncheck to point this out, just as it stayed quiet for <a href="https://importstatic.com/go/go-1-27-3-x509-distrust">the Go 1.27.3 certificate distrust</a> at first. The Go vulnerability database has <a href="https://pkg.go.dev/search?q=github.com%2Fandybalholm%2Fbrotli&amp;m=vuln">no report for andybalholm/brotli</a>, and govulncheck v1.9.0 on that module listed an authorization bypass in fasthttp's path handling (GO-2026-4950, fixed in v1.70.0) and two module-level findings, nothing about Brotli. The same run is a reason to move fasthttp forward anyway:</p>
<pre><code class="language-sh">go get github.com/valyala/fasthttp@latest      # v1.74.0+ no longer uses andybalholm/brotli
go get github.com/andybalholm/brotli@v1.2.7    # if anything else still imports it
</code></pre>
<p>After <code>go get github.com/andybalholm/brotli@v1.2.7</code> the Fiber test above decoded the same body unchanged. Neither the disclosure site nor any advisory we found mentions go-brrr, so we have nothing to report about it.</p>
<p>The C path in Go is <code>github.com/google/brotli/go/cbrotli</code>. Its <a href="https://github.com/google/brotli/blob/master/go/cbrotli/cgo.go"><code>cgo.go</code></a> links <code>libbrotlicommon libbrotlidec libbrotlienc</code> through pkg-config, so a binary built with it depends on the <code>libbrotli</code> on the build or runtime machine. The fix there is updating the OS or Homebrew package; <code>go get</code> won't change it.</p>
<p>To check binaries that are already deployed, read the module list Go embeds in them:</p>
<pre><code class="language-sh">go version -m ./server | grep -E 'brotli|fasthttp|fiber'
</code></pre>
<p>On our Fiber test build that printed <code>github.com/andybalholm/brotli v1.1.0</code> and <code>github.com/valyala/fasthttp v1.51.0</code>. A <code>github.com/google/brotli/go/cbrotli</code> dependency shows up in the same list.</p>
<h2>Rust: actix-web decodes by default</h2>
<p>The same test in Rust used 1.99.1 with actix-web 4.18.1 on its default features, plus axum 0.8 with and without tower-http's <code>RequestDecompressionLayer</code>:</p>
<pre><code class="language-text">compressed body:   28 bytes
actix-web handler: 33 bytes, decoded: true
(no layer)   axum handler:      28 bytes, decoded: false
(with layer) axum handler:      33 bytes, decoded: true
</code></pre>
<p>actix-web decompresses <code>br</code> request bodies with no configuration; axum does it only when you add the layer. Both use Dropbox's pure-Rust decoder:</p>
<pre><code class="language-text">$ cargo tree -i brotli-decompressor@6.0.1 -e normal
brotli-decompressor v6.0.1
└── brotli v9.0.0
    ├── actix-http v3.19.0
    │   └── actix-web v4.18.1
    │       └── rscheck v0.1.0
    └── compression-codecs v0.4.46
        └── async-compression v0.4.51
            └── tower-http v0.6.11
                └── rscheck v0.1.0
</code></pre>
<p>The disclosure site says this decoder returns an error for the malformed input, so the common Rust web stacks aren't in the code-execution group. Its repository has no CVE-related commit since disclosure, which is consistent with that.</p>
<p>In Rust, the exposure is in the C bindings. <code>brotlic</code> 0.8.2, the latest release (January 2024), depends on <code>brotlic-sys</code> 0.2.2, whose <code>build.rs</code> always compiles the bundled C sources with <code>cc</code>, and the bundled <code>version.h</code> says <code>#define BROTLI_VERSION 0x1000009</code>, which is Brotli 1.0.9, inside the affected range. There's no system-library option to fall back on, so replacing the crate or patching the vendored source is the fix until a new release ships. The older <code>brotli-sys</code> 0.3.2 (2017) bundles Brotli 0.6.0, which predates the advisory's 1.0.3 lower bound, though a snapshot from 2017 is worth replacing for its own sake.</p>
<p>Find either one with:</p>
<pre><code class="language-sh">cargo tree -i brotlic-sys
cargo tree -i brotli-sys
</code></pre>
<h2>HTTP clients</h2>
<p>The decoder also runs on responses your service fetches from other hosts, not only on request bodies. In Go, the default client doesn't decode <code>br</code> (see the <code>net/http</code> run above), so check for code that wraps a response body in a Brotli reader. In Rust, look for the same crates in the dependency tree of your HTTP client. If the decoder is a pure Go or Rust port, apply the version bumps above. If it is the C library in any form, it needs 1.0.10, 1.1.1 or 1.2.1 or later.</p>
<hr />
<p><em>Originally published on</em> <a href="https://importstatic.com/go/brotli-cve-2026-111497-go-rust"><em>ImportStatic</em></a><em>. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.</em></p>
]]></content:encoded></item></channel></rss>