CodeSampleX

샘플

undici 7.29.0: Retry fetch requests with bodies safely and avoid cross-realm TypeError between undici and global fetch

검증된 샘플 — npm undici 7.29.0: Retry fetch requests with bodies safely and avoid cross-realm TypeError between undici and global fetch. node 22 · linux…

sha256:58c13cc15f12e5f7e9c8dab8a50ff21446e25ffae7a78a5b505a95eae5b06e5e

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
2
빌드한 서명 키
2
선언된 환경 node linux x64 node node npm

검증 실행 환경

환경 컨트랙트 단계 실행일
node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-15
node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-18

케이스

HOW
목표
Retry fetch requests with bodies safely and avoid cross-realm TypeError between undici and global fetch
패키지
심벌
  • fetch
  • Request
  • Response
  • Headers
  • Request.clone
  • Request.bodyUsed
환경
node
생성일
2026-08-15T15:14:06Z

컨트랙트

  1. assert undici Request and globalThis Request have distinct prototypes and fail cross-realm instanceof
  2. assert passing globalThis.Request to undici.fetch or undici.Request to globalThis.fetch throws TypeError ERR_INVALID_URL
  3. assert fetch(req) consumes req.body marking req.bodyUsed as true
  4. assert retrying fetch(req) or calling req.clone() after fetch throws TypeError
  5. assert cloning Request before fetch allows successful retry with identical body

파일

  • NOTES.md
  • csx.json
  • package-lock.json
  • package.json
  • src/index.mjs
  • test/contract.mjs

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# CSX Sample Notes: undici fetch body consumption, retries & runtime globals

## 1. Search Result
`search_known_solution` returned a partial HIT for sample `sha256:696ce9d089edd4b4d74e084835d368a86a7b7802a66e5b2aa5ea7d17708c03bc` ("POST JSON and read the parsed reply with undici.request"). That existing sample only demonstrated high-level `undici.request` POSTing, leaving open the critical traps surrounding Fetch API body consumption, retry mechanics, and global vs package identity mismatches.

## 2. What a Model Would Have Written
A naive model would assume `globalThis.Request === undici.Request` (interchangeably passing global `Request` instances to `undici.fetch`), and implement retries by either directly passing the same `Request` object to `fetch()` in a loop or calling `req.clone()` after catching a 5xx response error.

## 3. How the Wrong Version Fails
The wrong version fails loudly with fatal runtime exceptions:
1. Passing a `globalThis.Request` into `undici.fetch()` (or `undici.Request` into `globalThis.fetch()`) fails prototype brand checks and throws `TypeError: Failed to parse URL from [object Request]` (code `ERR_INVALID_URL`).
2. Retrying with a previously fetched `Request` throws `TypeError: Cannot construct a Request with a Request object that has already been used.` because the body stream is already consumed (`bodyUsed = true`).
3. Calling `req.clone()` after a failed request throws `TypeError: unusable`.
csx.json
{"case":{"caseId":"case:sha256:00a49d32fe17e478cea81c71bf8254387817ba0f079f6d562c657e264eb52fc8","constraints":{"runtime":"node"},"contract":["assert undici Request and globalThis Request have distinct prototypes and fail cross-realm instanceof","assert passing globalThis.Request to undici.fetch or undici.Request to globalThis.fetch throws TypeError ERR_INVALID_URL","assert fetch(req) consumes req.body marking req.bodyUsed as true","assert retrying fetch(req) or calling req.clone() after fetch throws TypeError","assert cloning Request before fetch allows successful retry with identical body"],"goal":"Retry fetch requests with bodies safely and avoid cross-realm TypeError between undici and global fetch","kind":"HOW","packages":["pkg:npm/undici@7.29.0"],"schemaVersion":1,"symbols":["fetch","Request","Response","Headers","Request.clone","Request.bodyUsed"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/undici@7.29.0"],"schemaVersion":1,"symbols":["fetch","Request","Response","Headers","Request.clone","Request.bodyUsed"],"verifierAdapter":"node-typescript@1"}
package-lock.json
{
  "name": "csx-sample-undici-fetch-body",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "csx-sample-undici-fetch-body",
      "version": "1.0.0",
      "license": "MIT-0",
      "dependencies": {
        "undici": "7.29.0"
      }
    },
    "node_modules/undici": {
      "version": "7.29.0",
      "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
      "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
      "license": "MIT",
      "engines": {
        "node": ">=20.18.1"
      }
    }
  }
}
package.json
{
  "name": "csx-sample-undici-fetch-body",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "license": "MIT-0",
  "scripts": {
    "test": "node test/contract.mjs"
  },
  "dependencies": {
    "undici": "7.29.0"
  }
}
src/index.mjs
import { fetch as undiciFetch, Request as UndiciRequest } from 'undici';

/**
 * Retries a request when a transient failure occurs.
 *
 * TRAP 1: Request bodies are streams consumed exactly once. Calling fetch(req)
 * locks and consumes req.body, setting req.bodyUsed = true. Retrying with the
 * same Request object throws TypeError ("already used"). Calling req.clone()
 * after fetch fails with TypeError ("unusable"). To retry a Request with a body,
 * it must be cloned BEFORE dispatching.
 *
 * TRAP 2: In Node.js, globalThis.Request !== undici.Request.
 * Passing a global Request to undici.fetch() or an undici Request to
 * globalThis.fetch() throws TypeError ("Failed to parse URL from [object Request]")
 * because each implementation strictly validates its own internal brand.
 */

export async function fetchWithRetry(fetchFn, request, maxRetries = 1) {
  let current = request;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    // If retries remain and a body is present, clone before fetch consumes it
    const backup = (attempt < maxRetries && current.body)
      ? current.clone()
      : null;

    const response = await fetchFn(current);

    if (response.ok || attempt === maxRetries) {
      return response;
    }

    current = backup;
  }
}

export function isUndiciRequest(obj) {
  return obj instanceof UndiciRequest;
}

export function isGlobalRequest(obj) {
  return typeof globalThis.Request !== 'undefined' && obj instanceof globalThis.Request;
}
test/contract.mjs
import http from 'node:http';
import { strict as assert } from 'node:assert';
import {
  fetch as undiciFetch,
  Request as UndiciRequest,
  Response as UndiciResponse,
  Headers as UndiciHeaders
} from 'undici';
import { fetchWithRetry, isUndiciRequest, isGlobalRequest } from '../src/index.mjs';

// --- Part 1: Node globals vs undici exports identity trap ---
assert.notEqual(globalThis.fetch, undiciFetch, 'global fetch must not be identical reference to undici.fetch');
assert.notEqual(globalThis.Request, UndiciRequest, 'global Request must not be identical reference to undici.Request');
assert.notEqual(globalThis.Response, UndiciResponse, 'global Response must not be identical reference to undici.Response');
assert.notEqual(globalThis.Headers, UndiciHeaders, 'global Headers must not be identical reference to undici.Headers');

const undiciReq = new UndiciRequest('http://127.0.0.1:1234', { method: 'POST', body: 'sample' });
const globalReq = new globalThis.Request('http://127.0.0.1:1234', { method: 'POST', body: 'sample' });

assert.equal(isUndiciRequest(undiciReq), true);
assert.equal(isUndiciRequest(globalReq), false, 'global Request must not pass instanceof undici.Request');
assert.equal(isGlobalRequest(globalReq), true);
assert.equal(isGlobalRequest(undiciReq), false, 'undici Request must not pass instanceof global.Request');

// --- Part 2: Local HTTP server setup ---
const receivedRequests = [];
let attemptCount = 0;

const server = http.createServer((req, res) => {
  let body = '';
  req.on('data', (chunk) => { body += chunk; });
  req.on('end', () => {
    receivedRequests.push({ url: req.url, method: req.method, body });
    if (req.url === '/retry-endpoint') {
      attemptCount++;
      if (attemptCount === 1) {
        res.writeHead(503, { 'content-type': 'application/json' });
        res.end(JSON.stringify({ error: 'Service Unavailable' }));
        return;
      }
      res.writeHead(200, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ status: 'ok', attempt: attemptCount, echoed: JSON.parse(body) }));
      return;
    }

    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ ok: true, echoed: body }));
  });
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
const baseUrl = `http://127.0.0.1:${port}`;

try {
  // --- Part 3: Cross-realm fetch throws TypeError ERR_INVALID_URL ---
  await assert.rejects(
    async () => { await undiciFetch(new globalThis.Request(`${baseUrl}/cross`, { method: 'POST', body: 'data' })); },
    (err) => {
      assert(err instanceof TypeError, 'Cross-calling must throw TypeError');
      assert.match(err.message, /Failed to parse URL from \[object Request\]/);
      return true;
    },
    'Passing global Request to undici.fetch must fail brand check'
  );

  await assert.rejects(
    async () => { await globalThis.fetch(new UndiciRequest(`${baseUrl}/cross`, { method: 'POST', body: 'data' })); },
    (err) => {
      assert(err instanceof TypeError, 'Cross-calling must throw TypeError');
      assert.match(err.message, /Failed to parse URL from \[object Request\]/);
      return true;
    },
    'Passing undici Request to global fetch must fail brand check'
  );

  // --- Part 4: Body consumption exactly once & failure to retry naive Request ---
  const payload = { action: 'purchase', id: 42 };
  const singleUseReq = new UndiciRequest(`${baseUrl}/single-use`, {
    method: 'POST',
    body: JSON.stringify(payload),
    headers: { 'content-type': 'application/json' }
  });

  assert.equal(singleUseReq.bodyUsed, false, 'bodyUsed must be false before fetch');

  const res1 = await undiciFetch(singleUseReq);
  assert.equal(res1.status, 200);
  assert.equal(singleUseReq.bodyUsed, true, 'bodyUsed must be true after fetch has consumed body');

  // Attempting to retry with the same Request object throws TypeError
  await assert.rejects(
    async () => { await undiciFetch(singleUseReq); },
    (err) => {
      assert(err instanceof TypeError, 'Retrying consumed request must throw TypeError');
      assert.match(err.message, /already been used/i);
      return true;
    },
    'Reusing consumed Request object in fetch must throw'
  );

  // Attempting to clone after body is consumed throws TypeError ("unusable")
  assert.throws(
    () => { singleUseReq.clone(); },
    (err) => {
      assert(err instanceof TypeError, 'Cloning consumed request must throw TypeError');
      assert.match(err.message, /unusable/i);
      return true;
    },
    'Calling .clone() on consumed Request must throw'
  );

  // --- Part 5: Safe retry pattern via pre-dispatch clone ---
  const retryPayload = { transactionId: 'tx-99', amount: 100 };
  const retryReq = new UndiciRequest(`${baseUrl}/retry-endpoint`, {
    method: 'POST',
    body: JSON.stringify(retryPayload),
    headers: { 'content-type': 'application/json' }
  });

  const retryRes = await fetchWithRetry(undiciFetch, retryReq, 2);
  assert.equal(retryRes.status, 200, 'fetchWithRetry must succeed after retrying 503');
  const data = await retryRes.json();
  assert.equal(data.status, 'ok');
  assert.equal(data.attempt, 2);
  assert.deepEqual(data.echoed, retryPayload);

  // Verify server saw both requests with exact payload
  const retryRequests = receivedRequests.filter((r) => r.url === '/retry-endpoint');
  assert.equal(retryRequests.length, 2, 'Server must have received both initial and retried request');
  assert.deepEqual(JSON.parse(retryRequests[0].body), retryPayload);
  assert.deepEqual(JSON.parse(retryRequests[1].body), retryPayload);

  console.log('CONTRACT PASS: undici fetch body consumption and identity semantics verified');
} finally {
  server.close();
}

오리진 시더

anonymous