Vibe-guided bskyoauth and custom repo example code in Golang 🤖 probably not safe to use in prod

Fix body reader positioning in DPoP retry logic

The previous fix buffered the body but used bytes.NewBuffer which maintains
a read position. When retrying, we need to explicitly create a new
bytes.NewReader to ensure the read position starts at 0.

Changes:
- Explicitly set retryReq.Body with fresh bytes.NewReader
- Set ContentLength on retry request
- Update GetBody to return fresh bytes.NewReader instances

This ensures the PDS can read the full request body on retry attempts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+89 -1
internal
+9 -1
internal/dpop/transport.go
··· 66 66 67 67 // Restore body for the first request 68 68 req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) 69 + req.ContentLength = int64(len(bodyBytes)) 69 70 70 71 // Set GetBody so req.Clone() can recreate the body for retries 71 72 req.GetBody = func() (io.ReadCloser, error) { ··· 116 117 } 117 118 118 119 // Clone the request for retry 119 - // This will use GetBody to recreate the request body 120 + // Manually create a fresh request with a new body reader 120 121 retryReq := req.Clone(req.Context()) 122 + if len(bodyBytes) > 0 { 123 + retryReq.Body = io.NopCloser(bytes.NewReader(bodyBytes)) 124 + retryReq.ContentLength = int64(len(bodyBytes)) 125 + retryReq.GetBody = func() (io.ReadCloser, error) { 126 + return io.NopCloser(bytes.NewReader(bodyBytes)), nil 127 + } 128 + } 121 129 retryReq.Header.Set("DPoP", dpopProof) 122 130 retryReq.Header.Set("Authorization", "DPoP "+t.token) 123 131
+80
internal/dpop/transport_body_test.go
··· 1 + package dpop 2 + 3 + import ( 4 + "bytes" 5 + "crypto/ecdsa" 6 + "crypto/elliptic" 7 + "crypto/rand" 8 + "io" 9 + "net/http" 10 + "net/http/httptest" 11 + "testing" 12 + ) 13 + 14 + // TestTransportBodyRetry tests that request body is properly preserved for retries 15 + func TestTransportBodyRetry(t *testing.T) { 16 + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 17 + if err != nil { 18 + t.Fatalf("Failed to generate key: %v", err) 19 + } 20 + 21 + requestCount := 0 22 + var firstBody, secondBody []byte 23 + 24 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 + requestCount++ 26 + body, _ := io.ReadAll(r.Body) 27 + 28 + if requestCount == 1 { 29 + firstBody = body 30 + // First request - require nonce 31 + w.Header().Set("DPoP-Nonce", "test-nonce-123") 32 + w.WriteHeader(http.StatusUnauthorized) 33 + w.Write([]byte(`{"error":"use_dpop_nonce"}`)) 34 + } else { 35 + secondBody = body 36 + // Second request - success 37 + w.WriteHeader(http.StatusOK) 38 + w.Write([]byte(`{"success":true}`)) 39 + } 40 + })) 41 + defer server.Close() 42 + 43 + transport := NewTransport(http.DefaultTransport, key, "test-token", "") 44 + 45 + // Create request with body 46 + testBody := []byte(`{"test":"data","value":123}`) 47 + req, err := http.NewRequest("POST", server.URL, bytes.NewReader(testBody)) 48 + if err != nil { 49 + t.Fatalf("Failed to create request: %v", err) 50 + } 51 + req.Header.Set("Content-Type", "application/json") 52 + 53 + // Execute request 54 + resp, err := transport.RoundTrip(req) 55 + if err != nil { 56 + t.Fatalf("RoundTrip failed: %v", err) 57 + } 58 + defer resp.Body.Close() 59 + 60 + if resp.StatusCode != http.StatusOK { 61 + t.Errorf("Expected status 200, got %d", resp.StatusCode) 62 + } 63 + 64 + if requestCount != 2 { 65 + t.Errorf("Expected 2 requests (initial + retry), got %d", requestCount) 66 + } 67 + 68 + // Check that both requests received the same body 69 + if !bytes.Equal(firstBody, testBody) { 70 + t.Errorf("First request body mismatch.\nExpected: %s\nGot: %s", testBody, firstBody) 71 + } 72 + 73 + if !bytes.Equal(secondBody, testBody) { 74 + t.Errorf("Second request body mismatch.\nExpected: %s\nGot: %s", testBody, secondBody) 75 + } 76 + 77 + if !bytes.Equal(firstBody, secondBody) { 78 + t.Errorf("Request bodies differ between attempts.\nFirst: %s\nSecond: %s", firstBody, secondBody) 79 + } 80 + }