Egy upstream API válaszát gyakran két helyre is szeretnénk elküldeni: az egyik példány megy tovább a kliensnek, a másikat naplóznánk vagy feldolgoznánk. A Response.clone() erre szinte túlságosan kézenfekvőnek látszik. A metódus valóban létrehoz egy második Response-t, de nagy vagy lassan fogyó törzsnél rejtett memóriaköltséget hozhat be.

A clone nem másolatot készít előre
A fetch-válasz törzse stream: egyszer lehet elfogyasztani. A klónozás ezért nem egy második, teljesen lemásolt bájttömböt gyárt. A Fetch szabvány szerint a Response klónozása a törzset tee művelettel két ReadableStreamre bontja. Ugyanez a ReadableStream.tee() viselkedése: mindkét ág ugyanazokat a chunkokat kapja.
A fontos rész a backpressure. A standard tee nem a lassabb ághoz igazodik, hanem akkor kér új adatot a forrástól, amikor a gyorsabbik ág ezt lehetővé teszi. Emiatt a lemaradó ág sorában a még el nem olvasott chunkok gyűlnek. A Streams szabvány ezt kifejezetten leírja: ha csak az egyik ág fogy, a másik belső sora elvileg korlátlanul nőhet.
Gyanús minta például, amikor const auditCopy = upstream.clone() után az eredeti válasz közvetlenül a klienshez folyik, az auditpéldány pedig egy lassú távoli sinkre vagy későbbi feldolgozásra vár. A Response.clone() dokumentációja külön figyelmeztet arra, hogy ha az egyik body sokkal gyorsabban olvasódik, a fel nem használt adat a lassabb fogyasztó memóriájába pufferelődik.
Hol fáj ez igazán?
Kis JSON-válasznál rendszerint nem számít. Fájdalmas viszont fájlletöltést közvetítő BFF-ben, képfeldolgozó API előtt, exportoknál, SSE-szerű hosszan élő válasznál és Node-os proxyban. A memória nem egyetlen kéréshez kötött globális cache: több párhuzamos lassú klón összeadódik. Különösen félrevezető, hogy lokálisan egy rövid válasszal semmi nem látszik.
Node-ban sem érdemes ezt böngészős sajátosságnak tekinteni. A futtatókörnyezet a WHATWG Web Streams API-t adja a webstreams modulban, így egy ezen alapuló fetch- vagy proxykódban ugyanazt a tee-szemantikát kell fejben tartani.
A klónozás időzítése sem közömbös. Az eredeti response törzsének olvasása után a bodyUsed igaz lesz, és onnantól már nem lehet klónt kérni. Ez helyes működés: nincs mit újra elágaztatni. A tanulság nem az, hogy minden fetch után azonnal klónozzunk, hanem az, hogy a body felhasználását előre tervezzük meg. Egyetlen fogyasztó a legegyszerűbb és legolcsóbb adatfolyam.
Mit csinálj helyette?
- Ha csak diagnosztika kell, elsőként a státuszt, a Content-Type-ot, a Content-Length-et és egy kérésazonosítót naplózd. Ezekhez nem kell body-klón.
- Hibaágon, amikor a választ már nem küldöd tovább, olvass ki egy szigorúan korlátozott előnézetet, majd szakítsd meg az olvasást.
- Ha ugyanazt a teljes payloadot két rendszernek kell feldolgoznia, tárold el kontrollált helyre (például objektumtárba vagy ideiglenes fájlba), és onnan adj két olvasót. Ennek kapacitása és életciklusa tervezhető.
- Átfolyó válasznál ne adj automatikusan második fogyasztót. Ha mégis kell, terheld és figyeld a legrosszabb méret- és sebességkülönbséggel.
Az alábbi segédfüggvény a második ponthoz való. Csak olyan response bodyra használd, amelyet utána eldobnál: legfeljebb a megadott számú bájtot tartja meg, majd cancel()-lel leállítja a streamet. A chunk itt Fetch body esetén Uint8Array.
async function readPreview(body, byteLimit = 16 * 1024) {
const reader = body.getReader();
const parts = [];
let length = 0;
try {
while (length < byteLimit) {
const { done, value } = await reader.read();
if (done) break;
const part = value.subarray(0, byteLimit - length);
parts.push(part);
length += part.byteLength;
}
} finally {
await reader.cancel();
}
const joined = new Uint8Array(length);
let offset = 0;
for (const part of parts) {
joined.set(part, offset);
offset += part.byteLength;
}
return new TextDecoder().decode(joined);
}
A 16 KiB nem univerzális jó érték; ez egy szándékosan látható korlát. Bináris válasznál az előnézetet inkább hexben vagy base64-ben kezeld, és ne próbáld szöveggé dekódolni. A lényeg nem a konkrét limit, hanem hogy a klónozás előtt kimondjuk: ki fogyasztja a második ágat, milyen sebességgel, és mekkorára nőhet a sor.