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

このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。 合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。 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