anthropics / anthropics/anthropic-sdk-typescript

Retrying a request with a single-use stream body silently sends a 0-byte body and reports success

Ouverte
#1,170 1 commentaire 0 réactions 0 personnes assignées Voir sur GitHub
Langage dominant
TypeScript
Étoiles
2.1k
Forks
403
Merge moyen
1 j 21 h
PR mergées (30 j)
8

Description

### Describe the bug

When a request body is a single-use async iterable (or ReadableStream) and the first attempt fails with a retryable error (500/429/408/409/connection failure), the retry re-wraps the same exhausted iterator: the second attempt goes on the wire with a **0-byte body**, and the caller's promise **resolves successfully**. The payload is silently gone — no error, no warning.

In `buildBody` (`src/client.ts`), an async-iterable body is wrapped via `ReadableStreamFrom(options.body)` per attempt, but `options.body` itself is the caller's single-use iterator: attempt 1 drains it, and the retry gets an empty stream.

### Steps to reproduce

Self-contained script against `@anthropic-ai/sdk@0.121.0` — a loopback server fails the first POST with 500 and accepts the second; the client streams a 1024-byte body with `maxRetries: 1`:

repro (plain node, no dependencies beyond the SDK)

```js
// repro: node repro.mjs — loopback only, no external traffic
import http from 'node:http';
import { Anthropic } from '@anthropic-ai/sdk';

let n = 0;
const bodies = [];
const server = http.createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
n++;
bodies.push(Buffer.concat(chunks).toString('utf8'));
const body = n === 1 ? JSON.stringify({ error: { type: 'api_error' } }) : JSON.stringify({ id: 'msg_ok', type: 'message' });
res.writeHead(n === 1 ? 500 : 200, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) });
res.end(body);
});
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));

const client = new Anthropic({ apiKey: 'repro-local', baseURL: `http://127.0.0.1:${server.address().port}`, maxRetries: 1 });
async function* bodyIter() { yield 'A'.repeat(512); yield 'A'.repeat(512); } // 1024 bytes, single-use

const res = await client.post('/v1/anything', { body: bodyIter(), headers: { 'content-type': 'application/octet-stream' } });
console.log('attempts:', n, '| body lengths sent:', bodies.map((b) => Buffer.byteLength(b)));
console.log('caller sees success:', res && res.id === 'msg_ok');
server.close();
```

Output:

```
attempts: 2 | body lengths sent: [ 1024, 0 ]
caller sees success: true
```

### Expected behavior

Either the retry carries the same body as the original, or the retry is refused with a clear error when the body source is single-use. A silently-empty request that reports success is the worst of the three outcomes — the server may act on the truncated request (e.g. create an empty object) while the application believes its full payload was delivered.

### Environment

- `@anthropic-ai/sdk` 0.121.0
- Node 24 (also applies wherever streaming bodies are supported)

### Suggested fix

Buffer stream bodies up to a size cap for retry (as JSON bodies effectively are), or track that the body source is single-use and fail the retry loudly instead of sending an empty one.

Guide de contribution

Ouvrir le guide de contribution

Piste de recherche

Start with buildBody in src/client.ts and reproduce the supplied loopback-server script using a single-use async iterator and maxRetries: 1. Trace how the request body is rebuilt after the first failed attempt, then verify that the completed behavior either preserves the payload or reports a clear retry error instead of accepting an empty body.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
node.js, typescript
Domaine
api
Type d'issue
Bug
Difficulté
3/5
Temps estimé
1-2 jours
Activité
Active
Clarté
Clairement spécifiée
Accessibilité débutants
70/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.