anthropics / anthropics/anthropic-sdk-typescript

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

Đang mở
#1,170 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
TypeScript
Star
2.1k
Fork
403
Merge trung bình
1 ngày 21 giờ
Pull request đã merge (30 ngày)
8

Mô tả

### 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.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
node.js, typescript
Lĩnh vực
api
Loại issue
Lỗi
Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
70/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.