{"_id":"@bonniernews/fetchy","_rev":"5-f932dc739ae65a0dff8d1b3e8e63e114","name":"@bonniernews/fetchy","dist-tags":{"latest":"1.3.0"},"versions":{"1.0.0":{"name":"@bonniernews/fetchy","version":"1.0.0","keywords":["fetch","undici","cache","keepalive","agent"],"author":{"name":"Bonnier News"},"license":"MIT","_id":"@bonniernews/fetchy@1.0.0","maintainers":[{"name":"markusn","email":"markus@botten.org"},{"name":"mxtr","email":"max.olofsson@bonniernews.se"},{"name":"matsrorbecker","email":"mats@rorbecker.com"},{"name":"dan.karlsson","email":"dan.arola@bonniernews.se"},{"name":"adil.aboulkacim","email":"adil.aboulkacim@bonniernews.se"},{"name":"marie-winther","email":"marie.winther@bonniernews.se"},{"name":"marcusgronblad","email":"marcus.gronblad@bonniernews.se"},{"name":"axeljohanssonbonniernews","email":"axel.johansson@bonniernews.se"},{"name":"karlbergc","email":"christoffer.karlberg@bonniernews.se"},{"name":"chavah","email":"chavah.forler@bonniernews.se"},{"name":"joelekman","email":"joel.ekman@bonniernews.se"},{"name":"adam.hakansson","email":"adam.hakansson@bonniernews.se"},{"name":"hilleso","email":"marcus.hilleso@expressen.se"},{"name":"gusliden","email":"gusliden@gmail.com"},{"name":"andreas.samuelsson","email":"andreas.samuelsson@bonniernews.se"},{"name":"jzachrisson","email":"jesper.zachrisson@bonniernews.se"},{"name":"norla","email":"mattias.norlander@gmail.com"},{"name":"varneynz","email":"simon.varney@bonniernews.se"},{"name":"aliceboberg","email":"alice.boberg@bonniernews.se"},{"name":"johark","email":"johan.arkad@bonniernews.se"},{"name":"nifo","email":"niklas.forsstrom@bonniernews.se"},{"name":"herbola","email":"herman.jansson@bonniernews.se"},{"name":"vitryssen","email":"andre.nordlund@bonniernews.se"},{"name":"takolander","email":"william.takolander@bonniernews.se"},{"name":"morre","email":"marten.persson@hdsydsvenskan.se"},{"name":"mikael.mattsson","email":"mikael.mattsson@bonniernews.se"},{"name":"schristianssonbn","email":"seb.christiansson@bonniernews.se"},{"name":"mattiasobn","email":"mattias.olla@bonniernews.se"},{"name":"daghall","email":"markus@daghall.se"},{"name":"erandersson","email":"ericandersson1@gmail.com"},{"name":"jonaek","email":"jonathan.ek@bonniernews.se"},{"name":"amundsentb","email":"amundsentb@gmail.com"},{"name":"drgeobn","email":"daniel.rasmussen@bonniernews.se"},{"name":"oscartholander","email":"oscar@tholander.nu"},{"name":"peterpettersson","email":"peter.pettersson@bonniernews.se"},{"name":"liroliro","email":"basistoscar@gmail.com"},{"name":"torkelberget","email":"andreas.egneblad@bonniernews.se"},{"name":"johankasperi","email":"johan@kasperi.se"},{"name":"jon.eldeklint","email":"jon.eldeklint@bonniernews.se"},{"name":"nblom","email":"niklas.blom@bonniernews.se"},{"name":"jackesa","email":"jakob.pedersen@bonniernews.se"},{"name":"ollenolle","email":"olle.nordin@bonniernews.se"},{"name":"emilbjorklund","email":"emil@thatemil.com"},{"name":"lilianj","email":"japlilian@gmail.com"},{"name":"kristofferjansson","email":"kristoffer.jansson@bonniernews.se"},{"name":"kitgus","email":"kit.gustavsson@bonniernews.se"},{"name":"odynvolk","email":"alexi.rahman@r76.se"}],"homepage":"https://github.com/BonnierNews/fetchy#readme","bugs":{"url":"https://github.com/BonnierNews/fetchy/issues"},"dist":{"shasum":"5889aefde2c180003b45a598c5ece16a7fb26303","tarball":"https://registry.npmjs.org/@bonniernews/fetchy/-/fetchy-1.0.0.tgz","fileCount":13,"integrity":"sha512-naiwGeRDXnzenlXwql8nuOhwuRzbEgMapjyKIaWb2Ur2MnW06iT7T3GFCUXguwyOG1wqbThbEKqJ54bYdwA2Kw==","signatures":[{"sig":"MEUCIFd2ynILkvqsnyq0sJFx0afxW55Vk6ueNk6lo6jgepDXAiEAiwCJgDXhdu7txyV7TkHLsz7cwkHwMOxbfN9C8BHmh78=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":84160},"main":"index.js","type":"module","engines":{"node":">=22"},"gitHead":"2e02be7c19d98ebc0d5f8fe3fd77ecac58323a74","scripts":{"test":"mocha"},"_npmUser":{"name":"jon.eldeklint","email":"jon.eldeklint@bonniernews.se"},"overrides":{"serialize-javascript":"^7.0.5"},"repository":{"url":"git+https://github.com/BonnierNews/fetchy.git","type":"git"},"_npmVersion":"11.12.1","description":"A small pluggable fetch lib built on Node's native fetch stack (undici), with caching and keepAlive connection agents.","directories":{},"_nodeVersion":"24.15.0","dependencies":{"undici":"^7.0.0","lru-cache":"^6.0.0","exp-asynccache":"^3.2.0"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^4.3.6","nock":"^14.0.0","mocha":"^10.1.0","exp-fetch":"^6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/fetchy_1.0.0_1787749615796_0.13184433572413745","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@bonniernews/fetchy","version":"1.0.1","keywords":["fetch","undici","cache","keepalive","agent"],"author":{"name":"Bonnier News"},"license":"MIT","_id":"@bonniernews/fetchy@1.0.1","maintainers":[{"name":"markusn","email":"markus@botten.org"},{"name":"mxtr","email":"max.olofsson@bonniernews.se"},{"name":"matsrorbecker","email":"mats@rorbecker.com"},{"name":"dan.karlsson","email":"dan.arola@bonniernews.se"},{"name":"adil.aboulkacim","email":"adil.aboulkacim@bonniernews.se"},{"name":"marie-winther","email":"marie.winther@bonniernews.se"},{"name":"marcusgronblad","email":"marcus.gronblad@bonniernews.se"},{"name":"axeljohanssonbonniernews","email":"axel.johansson@bonniernews.se"},{"name":"karlbergc","email":"christoffer.karlberg@bonniernews.se"},{"name":"chavah","email":"chavah.forler@bonniernews.se"},{"name":"joelekman","email":"joel.ekman@bonniernews.se"},{"name":"adam.hakansson","email":"adam.hakansson@bonniernews.se"},{"name":"hilleso","email":"marcus.hilleso@expressen.se"},{"name":"gusliden","email":"gusliden@gmail.com"},{"name":"andreas.samuelsson","email":"andreas.samuelsson@bonniernews.se"},{"name":"jzachrisson","email":"jesper.zachrisson@bonniernews.se"},{"name":"norla","email":"mattias.norlander@gmail.com"},{"name":"varneynz","email":"simon.varney@bonniernews.se"},{"name":"aliceboberg","email":"alice.boberg@bonniernews.se"},{"name":"johark","email":"johan.arkad@bonniernews.se"},{"name":"nifo","email":"niklas.forsstrom@bonniernews.se"},{"name":"herbola","email":"herman.jansson@bonniernews.se"},{"name":"vitryssen","email":"andre.nordlund@bonniernews.se"},{"name":"takolander","email":"william.takolander@bonniernews.se"},{"name":"morre","email":"marten.persson@hdsydsvenskan.se"},{"name":"mikael.mattsson","email":"mikael.mattsson@bonniernews.se"},{"name":"schristianssonbn","email":"seb.christiansson@bonniernews.se"},{"name":"mattiasobn","email":"mattias.olla@bonniernews.se"},{"name":"daghall","email":"markus@daghall.se"},{"name":"erandersson","email":"ericandersson1@gmail.com"},{"name":"jonaek","email":"jonathan.ek@bonniernews.se"},{"name":"amundsentb","email":"amundsentb@gmail.com"},{"name":"drgeobn","email":"daniel.rasmussen@bonniernews.se"},{"name":"oscartholander","email":"oscar@tholander.nu"},{"name":"peterpettersson","email":"peter.pettersson@bonniernews.se"},{"name":"liroliro","email":"basistoscar@gmail.com"},{"name":"torkelberget","email":"andreas.egneblad@bonniernews.se"},{"name":"johankasperi","email":"johan@kasperi.se"},{"name":"jon.eldeklint","email":"jon.eldeklint@bonniernews.se"},{"name":"nblom","email":"niklas.blom@bonniernews.se"},{"name":"jackesa","email":"jakob.pedersen@bonniernews.se"},{"name":"ollenolle","email":"olle.nordin@bonniernews.se"},{"name":"emilbjorklund","email":"emil@thatemil.com"},{"name":"lilianj","email":"japlilian@gmail.com"},{"name":"kristofferjansson","email":"kristoffer.jansson@bonniernews.se"},{"name":"kitgus","email":"kit.gustavsson@bonniernews.se"},{"name":"odynvolk","email":"alexi.rahman@r76.se"}],"homepage":"https://github.com/BonnierNews/fetchy#readme","bugs":{"url":"https://github.com/BonnierNews/fetchy/issues"},"dist":{"shasum":"6747a5901a396bbd46786077a6515b1a5533e301","tarball":"https://registry.npmjs.org/@bonniernews/fetchy/-/fetchy-1.0.1.tgz","fileCount":13,"integrity":"sha512-x/7EHDhPAUtxCvPjLvVAglTk02aKLwq6SokPXHjUkolp/exfCe3YLIfje9nE5agQitQ5twXft3eKh6U9anmxqg==","signatures":[{"sig":"MEQCIAuhCxEXHmetRabiejvfFSHpPmaQoUDP48xDTgo8BhKKAiBWKA7T5HIgiqlmWKl617Yxc+Vj6Im8m/MSXCQWTp+5ug==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIEq3QkSMobjXUZBu4fEBSIW7mKScDnK0g0oTUOsB36mRAiA1Fwkekdcmq6399H3aN2g9J3vQG9701+MiDl0EQvHYbg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":84400},"main":"index.js","type":"module","engines":{"node":">=22"},"gitHead":"91b98e2d5cb9be3750e58c3ce865ef4c4065067b","scripts":{"test":"mocha"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:e75e8825-2214-46fb-b8a3-f370b247ff17"}},"overrides":{"serialize-javascript":"^7.0.5"},"repository":{"url":"git+https://github.com/BonnierNews/fetchy.git","type":"git"},"_npmVersion":"11.19.0","description":"Experimental: a small pluggable fetch lib built on Node's native fetch stack (undici), with caching and keepAlive connection agents.","directories":{},"_nodeVersion":"24.19.0","dependencies":{"undici":"^7.0.0","lru-cache":"^6.0.0","exp-asynccache":"^3.2.0"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^4.3.6","nock":"^14.0.0","mocha":"^10.1.0","exp-fetch":"^6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/fetchy_1.0.1_1787776360350_0.19063820240376872","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@bonniernews/fetchy","version":"1.1.0","keywords":["fetch","undici","cache","keepalive","agent"],"author":{"name":"Bonnier News"},"license":"MIT","_id":"@bonniernews/fetchy@1.1.0","maintainers":[{"name":"markusn","email":"markus@botten.org"},{"name":"mxtr","email":"max.olofsson@bonniernews.se"},{"name":"matsrorbecker","email":"mats@rorbecker.com"},{"name":"dan.karlsson","email":"dan.arola@bonniernews.se"},{"name":"adil.aboulkacim","email":"adil.aboulkacim@bonniernews.se"},{"name":"marie-winther","email":"marie.winther@bonniernews.se"},{"name":"marcusgronblad","email":"marcus.gronblad@bonniernews.se"},{"name":"axeljohanssonbonniernews","email":"axel.johansson@bonniernews.se"},{"name":"karlbergc","email":"christoffer.karlberg@bonniernews.se"},{"name":"chavah","email":"chavah.forler@bonniernews.se"},{"name":"joelekman","email":"joel.ekman@bonniernews.se"},{"name":"adam.hakansson","email":"adam.hakansson@bonniernews.se"},{"name":"hilleso","email":"marcus.hilleso@expressen.se"},{"name":"gusliden","email":"gusliden@gmail.com"},{"name":"andreas.samuelsson","email":"andreas.samuelsson@bonniernews.se"},{"name":"jzachrisson","email":"jesper.zachrisson@bonniernews.se"},{"name":"norla","email":"mattias.norlander@gmail.com"},{"name":"varneynz","email":"simon.varney@bonniernews.se"},{"name":"aliceboberg","email":"alice.boberg@bonniernews.se"},{"name":"johark","email":"johan.arkad@bonniernews.se"},{"name":"nifo","email":"niklas.forsstrom@bonniernews.se"},{"name":"herbola","email":"herman.jansson@bonniernews.se"},{"name":"vitryssen","email":"andre.nordlund@bonniernews.se"},{"name":"takolander","email":"william.takolander@bonniernews.se"},{"name":"morre","email":"marten.persson@hdsydsvenskan.se"},{"name":"mikael.mattsson","email":"mikael.mattsson@bonniernews.se"},{"name":"schristianssonbn","email":"seb.christiansson@bonniernews.se"},{"name":"mattiasobn","email":"mattias.olla@bonniernews.se"},{"name":"daghall","email":"markus@daghall.se"},{"name":"erandersson","email":"ericandersson1@gmail.com"},{"name":"jonaek","email":"jonathan.ek@bonniernews.se"},{"name":"amundsentb","email":"amundsentb@gmail.com"},{"name":"drgeobn","email":"daniel.rasmussen@bonniernews.se"},{"name":"oscartholander","email":"oscar@tholander.nu"},{"name":"peterpettersson","email":"peter.pettersson@bonniernews.se"},{"name":"liroliro","email":"basistoscar@gmail.com"},{"name":"torkelberget","email":"andreas.egneblad@bonniernews.se"},{"name":"johankasperi","email":"johan@kasperi.se"},{"name":"jon.eldeklint","email":"jon.eldeklint@bonniernews.se"},{"name":"nblom","email":"niklas.blom@bonniernews.se"},{"name":"jackesa","email":"jakob.pedersen@bonniernews.se"},{"name":"ollenolle","email":"olle.nordin@bonniernews.se"},{"name":"emilbjorklund","email":"emil@thatemil.com"},{"name":"lilianj","email":"japlilian@gmail.com"},{"name":"kristofferjansson","email":"kristoffer.jansson@bonniernews.se"},{"name":"kitgus","email":"kit.gustavsson@bonniernews.se"},{"name":"odynvolk","email":"alexi.rahman@r76.se"}],"homepage":"https://github.com/BonnierNews/fetchy#readme","bugs":{"url":"https://github.com/BonnierNews/fetchy/issues"},"dist":{"shasum":"fb384de1cde6392d3c807a1dab7e0a4e619b6c9a","tarball":"https://registry.npmjs.org/@bonniernews/fetchy/-/fetchy-1.1.0.tgz","fileCount":13,"integrity":"sha512-1d8wMJ4UyWIOmMdJ90P26671L1182SKl5p24Xhm3zRILtW8/TnUmdFU4tUczS/F1BAvFIV9dEUKnIbraRKaMOw==","signatures":[{"sig":"MEUCIGTuZJzrszsvvkrdZkLTgFT1+DeC0RPGXmeo9Y2lZdnHAiEAx/dITUz22fYP7/ZHB8dQorL/Oj6TbRR1Y2ZOISkEuno=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDQFuegXR/C6RLgB6t8LHZ7Fk1yzRENvtqFduPTU8JJXAIgX9Cm4kV5oK6OqGLiLDejL6BQuMHJNzUWFPbXEnVYpaY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":89507},"main":"index.js","type":"module","engines":{"node":">=22"},"gitHead":"6d619062f7aad2b86716cc98306c9f4453cf71a0","scripts":{"test":"mocha"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:e75e8825-2214-46fb-b8a3-f370b247ff17"}},"overrides":{"serialize-javascript":"^7.0.5"},"repository":{"url":"git+https://github.com/BonnierNews/fetchy.git","type":"git"},"_npmVersion":"11.19.1","description":"Experimental: a small pluggable fetch lib built on Node's native fetch stack (undici), with caching and keepAlive connection agents.","directories":{},"_nodeVersion":"24.19.0","dependencies":{"undici":"^7.0.0","lru-cache":"^6.0.0","exp-asynccache":"^3.2.0"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^4.3.6","nock":"^14.0.0","mocha":"^10.1.0","exp-fetch":"^6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/fetchy_1.1.0_1788513400213_0.8927793584856629","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@bonniernews/fetchy","version":"1.2.0","keywords":["fetch","undici","cache","keepalive","agent"],"author":{"name":"Bonnier News"},"license":"MIT","_id":"@bonniernews/fetchy@1.2.0","maintainers":[{"name":"markusn","email":"markus@botten.org"},{"name":"mxtr","email":"max.olofsson@bonniernews.se"},{"name":"matsrorbecker","email":"mats@rorbecker.com"},{"name":"dan.karlsson","email":"dan.arola@bonniernews.se"},{"name":"adil.aboulkacim","email":"adil.aboulkacim@bonniernews.se"},{"name":"marie-winther","email":"marie.winther@bonniernews.se"},{"name":"marcusgronblad","email":"marcus.gronblad@bonniernews.se"},{"name":"axeljohanssonbonniernews","email":"axel.johansson@bonniernews.se"},{"name":"karlbergc","email":"christoffer.karlberg@bonniernews.se"},{"name":"chavah","email":"chavah.forler@bonniernews.se"},{"name":"joelekman","email":"joel.ekman@bonniernews.se"},{"name":"adam.hakansson","email":"adam.hakansson@bonniernews.se"},{"name":"hilleso","email":"marcus.hilleso@expressen.se"},{"name":"gusliden","email":"gusliden@gmail.com"},{"name":"andreas.samuelsson","email":"andreas.samuelsson@bonniernews.se"},{"name":"jzachrisson","email":"jesper.zachrisson@bonniernews.se"},{"name":"norla","email":"mattias.norlander@gmail.com"},{"name":"varneynz","email":"simon.varney@bonniernews.se"},{"name":"aliceboberg","email":"alice.boberg@bonniernews.se"},{"name":"johark","email":"johan.arkad@bonniernews.se"},{"name":"nifo","email":"niklas.forsstrom@bonniernews.se"},{"name":"herbola","email":"herman.jansson@bonniernews.se"},{"name":"vitryssen","email":"andre.nordlund@bonniernews.se"},{"name":"takolander","email":"william.takolander@bonniernews.se"},{"name":"morre","email":"marten.persson@hdsydsvenskan.se"},{"name":"mikael.mattsson","email":"mikael.mattsson@bonniernews.se"},{"name":"schristianssonbn","email":"seb.christiansson@bonniernews.se"},{"name":"mattiasobn","email":"mattias.olla@bonniernews.se"},{"name":"daghall","email":"markus@daghall.se"},{"name":"erandersson","email":"ericandersson1@gmail.com"},{"name":"jonaek","email":"jonathan.ek@bonniernews.se"},{"name":"amundsentb","email":"amundsentb@gmail.com"},{"name":"drgeobn","email":"daniel.rasmussen@bonniernews.se"},{"name":"oscartholander","email":"oscar@tholander.nu"},{"name":"peterpettersson","email":"peter.pettersson@bonniernews.se"},{"name":"liroliro","email":"basistoscar@gmail.com"},{"name":"torkelberget","email":"andreas.egneblad@bonniernews.se"},{"name":"johankasperi","email":"johan@kasperi.se"},{"name":"jon.eldeklint","email":"jon.eldeklint@bonniernews.se"},{"name":"nblom","email":"niklas.blom@bonniernews.se"},{"name":"jackesa","email":"jakob.pedersen@bonniernews.se"},{"name":"ollenolle","email":"olle.nordin@bonniernews.se"},{"name":"emilbjorklund","email":"emil@thatemil.com"},{"name":"lilianj","email":"japlilian@gmail.com"},{"name":"kristofferjansson","email":"kristoffer.jansson@bonniernews.se"},{"name":"kitgus","email":"kit.gustavsson@bonniernews.se"},{"name":"odynvolk","email":"alexi.rahman@r76.se"}],"homepage":"https://github.com/BonnierNews/fetchy#readme","bugs":{"url":"https://github.com/BonnierNews/fetchy/issues"},"dist":{"shasum":"5a8c673b9eef8a0a1f91c6f5331f4dc28337522f","tarball":"https://registry.npmjs.org/@bonniernews/fetchy/-/fetchy-1.2.0.tgz","fileCount":13,"integrity":"sha512-iphH2X6yErR8Y0+5ToBv8t8QNP/CKgcawU9lt9S8yUBd8PBFTnjLvFUnj/sF6Cb1AySzNRAXzyx0RnULRrKe1g==","signatures":[{"sig":"MEYCIQD1R25vBqp5boqzkdb5vKDhu1wpn/0q5Pv4nXJw3Tib+gIhAJk8H4aujO9Ug8g0MjtaDT8Z8X+zRuyfIL7PEQgNITEc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIG36/83Man9GACZpfYHcpFMojF2cnA7QNDKjVh39+PlrAiAdikge0mTHKBkevBW8wuNCb4Fgbf0d3rv3cYAXDAYtzA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91887},"main":"index.js","type":"module","engines":{"node":">=22"},"gitHead":"8c5c2fbf5cb4925eafd48dfc0658f94609fd0b96","scripts":{"test":"mocha"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:e75e8825-2214-46fb-b8a3-f370b247ff17"}},"overrides":{"serialize-javascript":"^7.0.5"},"repository":{"url":"git+https://github.com/BonnierNews/fetchy.git","type":"git"},"_npmVersion":"11.19.1","description":"Experimental: a small pluggable fetch lib built on Node's native fetch stack (undici), with caching and keepAlive connection agents.","directories":{},"_nodeVersion":"24.20.0","dependencies":{"undici":"^7.0.0","lru-cache":"^6.0.0","exp-asynccache":"^3.2.0"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^4.3.6","nock":"^14.0.0","mocha":"^10.1.0","exp-fetch":"^6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/fetchy_1.2.0_1788790021340_0.609338999978569","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"_id":"@bonniernews/fetchy@1.3.0","bugs":{"url":"https://github.com/BonnierNews/fetchy/issues"},"dist":{"shasum":"0333edb13d420828feb1154ece04213d84a0679b","tarball":"https://registry.npmjs.org/@bonniernews/fetchy/-/fetchy-1.3.0.tgz","fileCount":13,"integrity":"sha512-8lUa2qH0sn4vyLlQMLzsTa4GLIIL8JgPjQUsmREVV64jpjJrRwrfVNwQuSe3ijjz6qZykbEP3NHZuV8OImffrA==","signatures":[{"sig":"MEQCIHl9hAg25sDK+8NFOsnOpgQcuD4/+o6m+uKTwQsw8GSWAiB2v4iwTY5gm5ZLJS/C580BUsrJ8fvYW/LGeCIhgSAQfg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCtbIAe28Na0V2/qAi2LBp57iorrtvUoP8Ed93i3IIW/wIhAN8Zbx8rHCnrb/+6ahmbb0ObjtIx++mAN7MPmHlcmVoG"}],"unpackedSize":94928},"main":"index.js","name":"@bonniernews/fetchy","type":"module","author":{"name":"Bonnier News"},"engines":{"node":">=22"},"gitHead":"ab4cc1bd29a4351f822b85816922dfee6c1b1e0b","license":"MIT","scripts":{"test":"mocha"},"version":"1.3.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:e75e8825-2214-46fb-b8a3-f370b247ff17"}},"homepage":"https://github.com/BonnierNews/fetchy#readme","keywords":["fetch","undici","cache","keepalive","agent"],"overrides":{"serialize-javascript":"^7.0.5"},"repository":{"url":"git+https://github.com/BonnierNews/fetchy.git","type":"git"},"_npmVersion":"11.19.1","description":"Experimental: a small pluggable fetch lib built on Node's native fetch stack (undici), with caching and keepAlive connection agents.","directories":{},"maintainers":[{"name":"markusn","email":"markus@botten.org"},{"name":"mxtr","email":"max.olofsson@bonniernews.se"},{"name":"matsrorbecker","email":"mats@rorbecker.com"},{"name":"dan.karlsson","email":"dan.arola@bonniernews.se"},{"name":"adil.aboulkacim","email":"adil.aboulkacim@bonniernews.se"},{"name":"marie-winther","email":"marie.winther@bonniernews.se"},{"name":"marcusgronblad","email":"marcus.gronblad@bonniernews.se"},{"name":"axeljohanssonbonniernews","email":"axel.johansson@bonniernews.se"},{"name":"karlbergc","email":"christoffer.karlberg@bonniernews.se"},{"name":"chavah","email":"chavah.forler@bonniernews.se"},{"name":"joelekman","email":"joel.ekman@bonniernews.se"},{"name":"adam.hakansson","email":"adam.hakansson@bonniernews.se"},{"name":"hilleso","email":"marcus.hilleso@expressen.se"},{"name":"gusliden","email":"gusliden@gmail.com"},{"name":"andreas.samuelsson","email":"andreas.samuelsson@bonniernews.se"},{"name":"jzachrisson","email":"jesper.zachrisson@bonniernews.se"},{"name":"norla","email":"mattias.norlander@gmail.com"},{"name":"varneynz","email":"simon.varney@bonniernews.se"},{"name":"aliceboberg","email":"alice.boberg@bonniernews.se"},{"name":"johark","email":"johan.arkad@bonniernews.se"},{"name":"nifo","email":"niklas.forsstrom@bonniernews.se"},{"name":"herbola","email":"herman.jansson@bonniernews.se"},{"name":"vitryssen","email":"andre.nordlund@bonniernews.se"},{"name":"takolander","email":"william.takolander@bonniernews.se"},{"name":"morre","email":"marten.persson@hdsydsvenskan.se"},{"name":"mikael.mattsson","email":"mikael.mattsson@bonniernews.se"},{"name":"schristianssonbn","email":"seb.christiansson@bonniernews.se"},{"name":"mattiasobn","email":"mattias.olla@bonniernews.se"},{"name":"daghall","email":"markus@daghall.se"},{"name":"erandersson","email":"ericandersson1@gmail.com"},{"name":"jonaek","email":"jonathan.ek@bonniernews.se"},{"name":"amundsentb","email":"amundsentb@gmail.com"},{"name":"drgeobn","email":"daniel.rasmussen@bonniernews.se"},{"name":"oscartholander","email":"oscar@tholander.nu"},{"name":"peterpettersson","email":"peter.pettersson@bonniernews.se"},{"name":"liroliro","email":"basistoscar@gmail.com"},{"name":"torkelberget","email":"andreas.egneblad@bonniernews.se"},{"name":"johankasperi","email":"johan@kasperi.se"},{"name":"jon.eldeklint","email":"jon.eldeklint@bonniernews.se"},{"name":"nblom","email":"niklas.blom@bonniernews.se"},{"name":"jackesa","email":"jakob.pedersen@bonniernews.se"},{"name":"ollenolle","email":"olle.nordin@bonniernews.se"},{"name":"emilbjorklund","email":"emil@thatemil.com"},{"name":"lilianj","email":"japlilian@gmail.com"},{"name":"kristofferjansson","email":"kristoffer.jansson@bonniernews.se"},{"name":"kitgus","email":"kit.gustavsson@bonniernews.se"},{"name":"odynvolk","email":"alexi.rahman@r76.se"}],"_nodeVersion":"24.20.0","dependencies":{"undici":"^7.0.0","lru-cache":"^6.0.0","exp-asynccache":"^3.2.0"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^4.3.6","nock":"^14.0.0","mocha":"^10.1.0","exp-fetch":"^6.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/fetchy_1.3.0_1788798131618_0.8034809462246113"}}},"time":{"created":"2026-08-26T13:06:55.686Z","modified":"2026-09-07T16:22:11.898Z","1.0.0":"2026-08-26T13:06:55.950Z","1.0.1":"2026-08-26T20:32:40.423Z","1.1.0":"2026-09-04T09:16:40.291Z","1.2.0":"2026-09-07T14:07:01.423Z","1.3.0":"2026-09-07T16:22:11.701Z"},"bugs":{"url":"https://github.com/BonnierNews/fetchy/issues"},"author":{"name":"Bonnier News"},"license":"MIT","homepage":"https://github.com/BonnierNews/fetchy#readme","keywords":["fetch","undici","cache","keepalive","agent"],"repository":{"url":"git+https://github.com/BonnierNews/fetchy.git","type":"git"},"description":"Experimental: a small pluggable fetch lib built on Node's native fetch stack (undici), with caching and keepAlive connection agents.","maintainers":[{"name":"markusn","email":"markus@botten.org"},{"name":"mxtr","email":"max.olofsson@bonniernews.se"},{"name":"matsrorbecker","email":"mats@rorbecker.com"},{"name":"dan.karlsson","email":"dan.arola@bonniernews.se"},{"name":"adil.aboulkacim","email":"adil.aboulkacim@bonniernews.se"},{"name":"marie-winther","email":"marie.winther@bonniernews.se"},{"name":"marcusgronblad","email":"marcus.gronblad@bonniernews.se"},{"name":"axeljohanssonbonniernews","email":"axel.johansson@bonniernews.se"},{"name":"karlbergc","email":"christoffer.karlberg@bonniernews.se"},{"name":"chavah","email":"chavah.forler@bonniernews.se"},{"name":"joelekman","email":"joel.ekman@bonniernews.se"},{"name":"adam.hakansson","email":"adam.hakansson@bonniernews.se"},{"name":"hilleso","email":"marcus.hilleso@expressen.se"},{"name":"gusliden","email":"gusliden@gmail.com"},{"name":"andreas.samuelsson","email":"andreas.samuelsson@bonniernews.se"},{"name":"jzachrisson","email":"jesper.zachrisson@bonniernews.se"},{"name":"norla","email":"mattias.norlander@gmail.com"},{"name":"varneynz","email":"simon.varney@bonniernews.se"},{"name":"aliceboberg","email":"alice.boberg@bonniernews.se"},{"name":"johark","email":"johan.arkad@bonniernews.se"},{"name":"nifo","email":"niklas.forsstrom@bonniernews.se"},{"name":"herbola","email":"herman.jansson@bonniernews.se"},{"name":"vitryssen","email":"andre.nordlund@bonniernews.se"},{"name":"takolander","email":"william.takolander@bonniernews.se"},{"name":"morre","email":"marten.persson@hdsydsvenskan.se"},{"name":"mikael.mattsson","email":"mikael.mattsson@bonniernews.se"},{"name":"schristianssonbn","email":"seb.christiansson@bonniernews.se"},{"name":"mattiasobn","email":"mattias.olla@bonniernews.se"},{"name":"daghall","email":"markus@daghall.se"},{"name":"erandersson","email":"ericandersson1@gmail.com"},{"name":"jonaek","email":"jonathan.ek@bonniernews.se"},{"name":"amundsentb","email":"amundsentb@gmail.com"},{"name":"drgeobn","email":"daniel.rasmussen@bonniernews.se"},{"name":"oscartholander","email":"oscar@tholander.nu"},{"name":"peterpettersson","email":"peter.pettersson@bonniernews.se"},{"name":"liroliro","email":"basistoscar@gmail.com"},{"name":"torkelberget","email":"andreas.egneblad@bonniernews.se"},{"name":"johankasperi","email":"johan@kasperi.se"},{"name":"jon.eldeklint","email":"jon.eldeklint@bonniernews.se"},{"name":"nblom","email":"niklas.blom@bonniernews.se"},{"name":"jackesa","email":"jakob.pedersen@bonniernews.se"},{"name":"ollenolle","email":"olle.nordin@bonniernews.se"},{"name":"emilbjorklund","email":"emil@thatemil.com"},{"name":"lilianj","email":"japlilian@gmail.com"},{"name":"kristofferjansson","email":"kristoffer.jansson@bonniernews.se"},{"name":"kitgus","email":"kit.gustavsson@bonniernews.se"},{"name":"odynvolk","email":"alexi.rahman@r76.se"}],"readme":"# @bonniernews/fetchy\n\n> ⚠️ **Experimental.** This package is under active development and not yet battle-tested\n> in production. APIs and behavior may change between releases — pin your version and\n> read the release notes before upgrading.\n\nA small, pluggable fetch lib built on **Node's native fetch stack (undici)** — with\nresponse caching and **keepAlive connection agents that are actually used**.\n\nIt's a drop-in-shaped replacement for [`exp-fetch`](https://github.com/BonnierNews/exp-fetch)\n(the `got`-based wrapper): same `buildFetch(behavior)` factory, same returned\nmethods, same callback/promise styles. The HTTP engine underneath is\n[`undici`](https://github.com/nodejs/undici) instead of `got`.\n\n- **ES modules**, Node 22+ (developed and tested on Node 24 LTS).\n- Minimal dependencies: `undici`, `exp-asynccache`, `lru-cache`.\n- **No global side effects**: importing (or using) fetchy never claims undici's\n  process-wide global dispatcher — every request carries its own dispatcher —\n  so built-in `fetch()` elsewhere in the process keeps running on Node's\n  bundled undici, unaffected by fetchy being present.\n- Connection pooling via a configurable undici `Agent` (works for http **and** https).\n\n## Install\n\n```sh\nnpm install @bonniernews/fetchy\n```\n\n## Usage\n\n```js\nimport buildFetch from \"@bonniernews/fetchy\";\n\nconst { fetch, get, post, del, stats } = buildFetch({ /* behavior */ });\n\n// Promise\nconst body = await fetch(\"https://example.com/resource.json\");\n\n// Callback\nfetch(\"https://example.com/resource.json\", (err, body) => { /* ... */ });\n\n// Verbs\nawait post(\"https://example.com/things\", { name: \"thing\" });\n```\n\nThe factory returns `fetch`, `get`, `post`, `put`, `patch`, `head`, `options`,\n`del`, and `stats()`. Each request method accepts either a URL string or an\noptions object `{ url, headers, timeout }`, an optional body, and an optional\ncallback. With no callback a promise is returned.\n\n## Connection agents (keepAlive)\n\nNative `fetch` ignores Node's `http.Agent`; pooling is done by an undici\n`Dispatcher`. This lib builds one keepAlive `Agent` per factory and uses it on\nevery request, so connections are reused (TLS handshakes included) for both\n`http://` and `https://`.\n\n```js\nimport { Agent } from \"undici\";\n\n// (a) pass tuning options — a default keepAlive Agent is built for you\nbuildFetch({ agent: { keepAliveTimeout: 10_000, connections: 10 } });\n\n// (b) pass a ready-made undici Dispatcher (used verbatim)\n// ⚠️ `rejectUnauthorized: false` disables TLS certificate verification — it\n// exposes you to man-in-the-middle attacks. Use it only against a local test\n// server, never in production.\nbuildFetch({ agent: new Agent({ connect: { rejectUnauthorized: false } }) });\n\n// (c) pass nothing — a keepAlive Agent with sane defaults is still created\nbuildFetch({});\n```\n\nHTTPS/TLS options go under the agent's `connect` key (e.g. `ca`, `rejectUnauthorized`).\n\nFor any Agent it builds, the lib creates **one shared undici connector** so the TLS\nsession cache is shared across connections — TLS sessions resume even on\nnon-pooled (`Connection: close`) workloads. If you pass a *pre-built* `undici.Agent`\nyourself, build it with a shared `buildConnector(...)` to get the same behavior.\n\n## Behavior options\n\nSame contract as `exp-fetch`:\n\n`freeze` (`true`), `deepFreeze` (`false`), `clone` (`true`), `cache`\n(AsyncCache, 60s; falsy disables), `cacheKeyFn`, `cacheValueFn`, `maxAgeFn`,\n`onNotFound`, `onError`, `onSuccess`, `onRequestInit`, `requestTimeFn`,\n`cacheNotFound` (`false`), `logger`, `getCorrelationId`, `correlationIdHeader`\n(`\"correlation-id\"`), `errorOnRemoteError` (`true`), `contentType` (`\"json\"`),\n`agent`, `followRedirect` (`true`), `httpMethod` (`\"GET\"`), `timeout` (`20000`),\n`headers`, `retry` (`0`), `varyHeaders` (`[]`), `maxResponseBytes` (`0`),\n`isRedirectAllowed`, `transport` (auto: `\"undici\"`, or `\"fetch\"` when\n`NODE_ENV === \"test\"`), `hooks.beforeRequest`.\n\nCaching, manual redirect following (up to 10 hops, with per-hop caching and the\n`followRedirect: false` `{ statusCode, headers }` return), `cache-control`\n`max-age` parsing, correlation-id / `User-Agent` / `x-exp-fetch-appname` headers,\nand `stats()` hit ratio all behave as in `exp-fetch`.\n\n### Default headers\n\nThe lib may inject these headers when the caller hasn't set them (the exp-fetch\nset, plus `accept-encoding`). The list is exported as `DEFAULT_HEADERS` (also\nattached to the default export) for tests/proxies that need to strip or assert\non lib-added headers:\n\n| header | value | when |\n|---|---|---|\n| `correlation-id` | from `getCorrelationId()` | when configured; name follows `correlationIdHeader` |\n| `x-cloud-trace-context` | from `getTraceContext()` | when configured; name follows `traceContextHeader` |\n| `x-exp-fetch-appname` | the app's `package.json` name | when the app has a name |\n| `user-agent` | `<app>/<version>` | unless caller-set |\n| `accept-encoding` | `gzip, deflate` | unless caller-set |\n\n```js\nimport buildFetch, { DEFAULT_HEADERS } from \"@bonniernews/fetchy\";\n```\n\n**`getTraceContext`** (new, opt-in) propagates distributed-tracing context the\nsame way `getCorrelationId` propagates the correlation id: supply a getter\n(typically AsyncLocalStorage-backed, reading the incoming request's header) and\nthe value rides on every outgoing request — on Cloud Run this is what stitches\nthe request tree together in Cloud Trace. The default header name is Cloud Run's\n`x-cloud-trace-context`; override with `traceContextHeader` (e.g. `\"traceparent\"`\nfor W3C trace context):\n\n```js\nbuildFetch({ getTraceContext: () => requestContext.get(\"traceContext\") });\n```\n\n## Security\n\nThe cache is a single, process-wide store. Without care, a shared cache leaks\ndata between callers and a redirect can leak credentials, so the defaults here\nare conservative:\n\n- **Cache key varies on credentials (and method).** The default cache key folds\n  in the HTTP method (so a `del()`/`post()` is never served a cached GET body)\n  and the `authorization`, `cookie`, and `proxy-authorization` headers, so one\n  caller's authenticated response is never served to another caller with\n  different (or no) credentials. Add more headers with\n  `varyHeaders: [\"accept-language\", ...]`. A custom `cacheKeyFn(url, body,\n  headers, method)` replaces this entirely — **you** own keying then, including\n  credential separation. (Even so,\n  don't put a personalized/private response in a long-lived shared cache unless\n  you've thought about who else holds the key.)\n- **Credentials are dropped on cross-origin redirects.** When a redirect crosses\n  to a different origin, the `authorization`, `cookie`, and `proxy-authorization`\n  headers are stripped before the next hop, so a redirect to another host can't\n  harvest them. Same-origin redirects keep them.\n- **`isRedirectAllowed(toUrl, fromUrl)`** — return a falsy value to block a hop\n  (rejects with `code: \"EREDIRECTBLOCKED\"`). Validating only the *initial* URL is\n  not enough to stop SSRF: a redirect can point at an internal or\n  cloud-metadata address. Use this to re-check every hop. Tightening further\n  (DNS-pinning, blocking private IP ranges) is the caller's responsibility.\n  ```js\n  buildFetch({ isRedirectAllowed: (to) => new URL(to).hostname !== \"169.254.169.254\" });\n  ```\n- **`maxResponseBytes`** (bytes, `0` = unbounded) — cap the response body read\n  into memory so a hostile or compromised upstream can't exhaust it. A\n  `Content-Length` over the cap is rejected up front; a missing or lying one is\n  caught while streaming (rejects with `code: \"EBODYTOOLARGE\"`). The streamed\n  cap counts **decompressed** bytes, so a compressed bomb can't defeat it.\n  **Off by default** to preserve drop-in behavior — set it when fetching\n  untrusted hosts.\n  ```js\n  buildFetch({ maxResponseBytes: 10 * 1024 * 1024 }); // 10 MiB\n  ```\n\nNote that URLs are logged (at `debug`/`info`) and embedded in error messages, so\navoid putting secrets in query strings. Response bodies in error messages are\ntruncated, but are still passed in full to your `onError` hook.\n\n## Resilience (opt-in)\n\nApp-layer resilience primitives for protecting backends, modelled on gaps an SRE\nreview found across our sites (no circuit breakers, no outbound concurrency caps,\nno stale-if-error, no disconnect propagation). All are **off by default** — when\nunset, the request path is unchanged. Per-host state lives for the lifetime of one\n`buildFetch()` factory (typically one backend).\n\n- **`staleIfError`** (seconds, or an `AsyncCache`): keep the last-good response\n  and serve it when a later request hits a **5xx, 429, timeout, or network\n  error**. Other 4xx, 404, and `EBODYTOOLARGE` are never masked; `onError` still\n  fires when a stale value masks a failure (so error-rate monitoring keeps\n  seeing the outage); the served value isn't re-cached as fresh. App-layer\n  `stale-if-error`.\n  A number of seconds keeps an internal in-process store. Pass your own\n  `AsyncCache` instance instead (the same contract as `cache`) to own the store:\n  inspect it, purge an entry (`del`), pre-seed it, share it between factories,\n  or back it with something remote — retention is then governed by the store's\n  own TTL. Entries are keyed by the same cache keys as the main cache\n  (`cacheKeyFn`), and it must **not** be the same instance as `cache` — colliding\n  keys would serve stale values as fresh, so that config is ignored with a\n  warning.\n  ```js\n  buildFetch({ staleIfError: 3600 }); // serve last-good for up to 1h on origin failure\n\n  // or own the store, e.g. to purge a bad last-good entry on demand:\n  import AsyncCache from \"exp-asynccache\";\n  const staleCache = new AsyncCache(buildFetch.initLRUCache({ age: 3600 }));\n  // NB: a url-only key drops the default credential-aware keying (for the main\n  // cache too) — only do this when requests carry no per-user credentials.\n  buildFetch({ staleIfError: staleCache, cacheKeyFn: (url) => url });\n  await staleCache.del(\"https://api.example/thing\");\n  ```\n- **`circuitBreaker`** (`true` or `{ failureThreshold = 5, resetTimeout = 30000, comparator, failureStatusCodes }`):\n  per-host breaker. After `failureThreshold` consecutive 5xx/timeout/network\n  failures it **opens** and fails fast (`Error` with `code: \"ECIRCUITOPEN\"`, or a\n  stale response if `staleIfError` is set) for `resetTimeout` ms, then admits one\n  half-open probe. 4xx/404 count as the backend being healthy — except **429,\n  which is neutral**: it neither counts as a failure (rate limits are usually\n  per key, not per origin) nor resets the failure counter (a backend flapping\n  500/429/500 still trips), and a half-open probe answered 429 doesn't close the\n  breaker — it stays half-open, probing one request at a time until a real\n  success closes it. When a backend rate-limits **per host**, opt into counting\n  throttling as failures with `failureStatusCodes: [429]`: listed statuses\n  (4xx only — 5xx always counts, 404 keeps its notFound handling) count toward\n  the threshold and route like a 5xx (masked by `staleIfError`, `onError` still\n  fires).\n  For services that answer **200 with an error payload**, `comparator(content, result)`\n  lets the resilience layer see through the status code: return truthy for healthy,\n  falsy to count that 2xx as a **failure**. A flagged 2xx counts toward the breaker,\n  is never cached as fresh (its `max-age` is ignored), and never becomes the\n  last-good value — and when `staleIfError` is enabled and a last-good response\n  exists, that is served instead (`stale.served`), exactly like a 5xx. Without one\n  the flagged body is still delivered, so existing app-level handling of soft\n  errors keeps working. A throwing comparator counts as healthy and rejects that\n  request only.\n  ```js\n  buildFetch({ circuitBreaker: { failureThreshold: 5, resetTimeout: 30000 } });\n  // backend rate-limits per host: sustained 429s should trip the breaker\n  buildFetch({ circuitBreaker: { failureStatusCodes: [ 429 ] } });\n  // backend hides errors behind a 200:\n  buildFetch({ circuitBreaker: { comparator: (content) => content?.status !== \"error\" } });\n  ```\n- **`maxConcurrentPerHost`** (+ optional **`maxQueuePerHost`**): cap in-flight\n  requests per origin to bound the `instances × maxSockets` connection multiplier.\n  Beyond the cap requests queue; with `maxQueuePerHost` set, a full queue **sheds**\n  (`Error` with `code: \"ECONCURRENCYLIMIT\"`). (undici's Agent `connections` caps\n  connections at the transport level; this adds request-level queueing/shedding.)\n  ```js\n  buildFetch({ maxConcurrentPerHost: 50, maxQueuePerHost: 100 });\n  ```\n- **Caller `signal`** (per request): pass an `AbortSignal` on the request options\n  to cancel in-flight work — e.g. wire `req.on(\"close\")` so a client disconnect\n  releases backend capacity. It's combined with the timeout; a caller abort is\n  **not** retried and surfaces as the abort, not `ESOCKETTIMEDOUT`. Concurrent\n  identical requests share one in-flight load: an abort rejects **only that\n  caller's** promise, and the shared wire request is cancelled when the **last**\n  interested caller aborts.\n  ```js\n  fetcher.get({ url, signal: req.signal });\n  ```\n\nThe default retry `calculateDelay` is **exponential with jitter** (capped at 3s)\nrather than linear, so a fleet retrying a flapping backend doesn't resynchronize\ninto load spikes. A custom `calculateDelay` still overrides it.\n\nWhen a retryable response carries a **`Retry-After`** header (delta-seconds or an\nHTTP-date), it overrides the backoff delay. A server asking for a longer wait than\n**`retry.maxRetryAfter`** (ms; defaults to the request timeout) is **not** retried —\nthe response is surfaced as-is:\n\n```js\nbuildFetch({ retry: { limit: 2, maxRetryAfter: 5000 } });\n// 429 + \"retry-after: 2\"  -> retried after 2s (backoff ignored)\n// 503 + \"retry-after: 60\" -> not retried, the 503 is surfaced immediately\n```\n\n### `onMetric` — resilience observability\n\nPass `onMetric(event)` to observe the resilience machinery (for counters,\ngauges, alerts). It's called **best-effort** — a throwing hook is swallowed (logged\nat `debug`) and never affects the request. Each `event` has a `type` and the fields\nbelow:\n\n| `type` | fields | when |\n|---|---|---|\n| `circuit.open` | `host` | breaker trips open (threshold hit, or half-open probe failed) |\n| `circuit.half_open` | `host` | breaker admits a probe after `resetTimeout` |\n| `circuit.close` | `host` | breaker recovers (probe succeeded) |\n| `circuit.rejected` | `host` | a request was short-circuited by an open breaker |\n| `concurrency.queued` | `host` | all slots busy — the request had to wait |\n| `concurrency.shed` | `host` | the per-host queue was full (`ECONCURRENCYLIMIT`) |\n| `stale.served` | `cacheKey` | a last-good response was served on failure |\n| `retry` | `url`, `attempt`, `reason` (`\"status\"`/`\"network\"`), `statusCode?`, `retryAfterMs?` | a retry was scheduled (`retryAfterMs` when a `Retry-After` header set the delay) |\n\n```js\nbuildFetch({\n  circuitBreaker: true,\n  staleIfError: 3600,\n  onMetric: (e) => {\n    if (e.type === \"circuit.open\") metrics.increment(\"breaker.open\", { host: e.host });\n    if (e.type === \"stale.served\") metrics.increment(\"stale.served\");\n  },\n});\n```\n\n`host` is the origin (e.g. `https://api.example`). Success/error/timing have their\nown hooks (`onSuccess`/`onError`/`onNotFound`, `requestTimeFn`) — `onMetric` is\nspecifically the resilience signal.\n\n## Callbacks\n\nThe lifecycle callbacks from exp-fetch, unchanged. `res` is the response\n(`{ statusCode, headers }`), `content` the parsed body, `cacheKey` the computed key.\n\n```js\nfunction onSuccess(url, cacheKey, res, content) {}   // 2xx\nfunction onError(url, cacheKey, res, content) {}      // > 299 (not 404)\nfunction onNotFound(url, cacheKey, res, content) {}   // 404\n```\n\n`onError` also fires when [`staleIfError`](#resilience-opt-in) serves a stale\nvalue in place of a 5xx or 429. With `errorOnRemoteError: false`, error responses\nresolve with `null` (like the 404 path).\n\n**`onRequestInit(requestOptions, cacheKey)`** runs **once** before the actual\nrequest is made (i.e. on a cache miss) — **not** on cache hits, and **not** on\nsubsequent redirect hops. `requestOptions` is a copy of the request options\n(`{ url, method, responseType, followRedirect, headers }`) and does not alter the\nrequest. Handy for mocking:\n\n```js\nimport nock from \"nock\";\n\nfunction onRequestInit(requestOptions, cacheKey) {\n  const { protocol, host, pathname } = new URL(requestOptions.url);\n  nock(`${protocol}//${host}`).get(pathname).reply(200, { mock: true });\n}\n\nconst { fetch } = buildFetch({ onRequestInit });\n```\n\n**`requestTimeFn(requestOptions, took)`** is called after each request with the\nelapsed milliseconds (the default logs it at `debug`):\n\n```js\nfunction requestTimeFn(requestOptions, took) {\n  console.log(\"REQUEST\", requestOptions.method, \":\", requestOptions.url, \"took\", took, \"ms\");\n}\n```\n\n### `hooks.beforeRequest`\n\nThe one got hook carried over (it's widely used to sign/decorate outgoing\nrequests). `hooks.beforeRequest` is an **array of functions** (a single function is\nalso accepted), each run — sync or async (awaited) — **before every wire\nattempt**: every hop (including redirects) and every retry attempt (got\nsemantics — timestamp/nonce signatures stay fresh on retries). Each receives a\nmutable request-options object `{ url, method, headers, body }`; mutate it in\nplace to change what's sent:\n\n```js\nconst { get } = buildFetch({\n  hooks: {\n    beforeRequest: [ (options) => { options.headers.authorization = sign(options); } ],\n  },\n});\n```\n\nIt runs *after* the cache key is computed, so mutating headers here does not affect\ncaching — use `cacheKeyFn`/`varyHeaders` for that. The options object is this lib's\nown shape (not a full got options object); got's other hooks\n(`afterResponse`, `beforeRetry`, …) are not supported — passing one logs a\nwarning at build time instead of being dropped silently.\n\n## Migrating from exp-fetch\n\n@bonniernews/fetchy is an API-compatible **superset** of\n[`exp-fetch`](https://github.com/BonnierNews/exp-fetch): same `buildFetch(behavior)`\nfactory, same returned methods (`fetch`/`get`/`post`/`put`/`patch`/`head`/`options`/\n`del`/`stats`), same `initLRUCache` export, and every exp-fetch behavior option is\nstill honored — including the `onSuccess`/`onError`/`onNotFound`/`onRequestInit`/\n`requestTimeFn` callbacks and `hooks.beforeRequest` (see [Callbacks](#callbacks)).\nMost apps change the import and nothing else.\n\n**Two changes you must make:**\n\n1. **Import as ESM.** It's an ES-module package (Node 22+):\n   ```diff\n   - const buildFetch = require(\"exp-fetch\");\n   + import buildFetch from \"@bonniernews/fetchy\";\n   ```\n   A CommonJS app can `await import(\"@bonniernews/fetchy\")` instead.\n2. **Bump `nock` to v14+** if you mock HTTP with it. exp-fetch ran on `got`/`http`,\n   which `nock@13` intercepts; @bonniernews/fetchy uses undici, which only `nock@14+`\n   reaches (via the global `fetch` the test transport uses). No test code changes\n   beyond the version bump — or inject undici's `MockAgent` as `agent` instead. See\n   [Testing with nock](#testing-with-nock).\n\n**Default-behavior changes to be aware of** (all are deliberate security/correctness\nfixes; the request/response shapes are otherwise unchanged):\n\n| Behavior | exp-fetch | @bonniernews/fetchy | Notes |\n|---|---|---|---|\n| Default cache key | URL + body | **method (non-GET) +** URL + body **+ `authorization`/`cookie`/`proxy-authorization`** | Verbs no longer share a cache entry (a `del()` after a cached `get()` reaches the backend), and credentialed requests no longer share one either (fixes cross-user leakage). A custom `cacheKeyFn` overrides this — see [Security](#security). |\n| `errorOnRemoteError: false` | resolves `undefined` | resolves `null` (like the 404 path) | Lets error responses be negatively cached (`undefined` can never be served from cache). |\n| Cross-origin redirect | forwards all headers | **strips** credential headers | Same-origin redirects unchanged. |\n| `cacheNotFound: true` | caches 404s for ~1 ms (`isNumber(true)` quirk) | caches 404s per the response's `cache-control` | Pass a number for an explicit TTL. |\n| `cache-control` parsing | substring match (`max-age=05`→non-cacheable) | per-directive (`max-age=05`→5 s) | Edge cases only. |\n| Remote-error message | full `util.inspect(body)` | body **truncated** | Prefix `\"<url> yielded <status>\"` is preserved; full body still reaches `onError`. |\n\n**New, opt-in** (off unless configured, so they don't change migration behavior):\n`staleIfError`, `circuitBreaker`, `maxConcurrentPerHost`/`maxQueuePerHost`, per-request\n`signal`, `varyHeaders`, `maxResponseBytes`, `isRedirectAllowed`, `transport`,\n`getTraceContext`/`traceContextHeader` — see [Security](#security),\n[Resilience (opt-in)](#resilience-opt-in), and [Default headers](#default-headers).\n\nSee the engine-level differences below for the rest.\n\n## Differences from the `got`-based exp-fetch\n\n- **Engine:** requests go through `undici.request` in production and the global\n  `fetch` under test (`NODE_ENV === \"test\"`), so `nock@14+` intercepts them out of\n  the box (see [Testing with nock](#testing-with-nock)); you can also inject\n  undici's `MockAgent` as `agent`. Override with `transport: \"undici\"` / `\"fetch\"`.\n- **`agent`:** must be an undici `Dispatcher` or an Agent-options object (all\n  undici Agent options pass through; the http.Agent-era `maxSockets` maps to\n  `connections`). A Node `http.Agent` is ignored. TLS options move to\n  `agent.connect`.\n- **Errors:** plain `Error` (with `.statusCode`), not `VError`. Timeout errors\n  carry `code: \"ESOCKETTIMEDOUT\"` (and the same message), with the underlying\n  abort as `.cause`.\n- **Timeout:** an object timeout (`{ socket, request, send, ... }` — got's phase\n  keys are recognized) collapses to a single deadline (the smallest provided\n  value) that covers the **whole exchange including the body read**. An explicit\n  `0` disables the deadline.\n- **`retry`:** a minimal policy (`limit`/`retries`, `methods`, `statusCodes`,\n  `calculateDelay`, `maxRetryAfter`) — honours `Retry-After` like got, but is not\n  got's full retry engine.\n- **`contentType`:** JSON and text only. **XML is not supported** (no `xml2js`).\n- **`hooks`:** only `hooks.beforeRequest` is supported (the one exp-fetch code\n  commonly uses); got's other hooks log a build-time warning. Plus the\n  `onRequestInit`/`onSuccess`/`onError`/`onNotFound`/`requestTimeFn` callbacks —\n  see [Callbacks](#callbacks).\n- **Redirects:** 303 becomes a body-less GET, and 301/302 rewrite POST to GET\n  (got/browser behavior); 307/308 keep method and body. A 3xx without a\n  `Location` (e.g. 304) is handled as a plain status, not followed.\n- **Cloning:** uses `structuredClone`; a non-cloneable payload falls back to the\n  original reference instead of throwing. With `deepFreeze: true` cache hits skip\n  the clone entirely and return the shared deep-frozen instance (it can't be\n  mutated anyway) — zero per-hit copy cost on hot keys.\n\n## Testing with nock\n\nUnder test (`NODE_ENV === \"test\"`) requests go through the global `fetch`, which\n[`nock@14+`](https://github.com/nock/nock) intercepts — so existing nock-based\ntest suites work with **no configuration**:\n\n```js\nimport nock from \"nock\";\nimport buildFetch from \"@bonniernews/fetchy\";\n\nconst { get } = buildFetch(); // NODE_ENV=test -> global fetch\nnock(\"http://api.example\").get(\"/thing\").reply(200, { ok: true });\nawait get(\"http://api.example/thing\"); // intercepted by nock\n```\n\nIn production the default is `undici.request` instead — it's ~1.5–2x faster on the\nkeepAlive path (the WHATWG `fetch` layer adds per-request overhead; see\n`bench/transport-bench.js`). nock doesn't intercept `undici.request`, but that\nonly matters in tests, where the default is already `fetch`.\n\nThe two transports are normalized to behave identically: caching, redirect\nfollowing, 404/error handling, `maxResponseBytes`, compression (both send\n`accept-encoding: gzip, deflate` and transparently decompress gzip/deflate/br),\nrepeated headers (comma-joined strings, except `Set-Cookie` which is an array on\nboth), and the resilience primitives. The keepAlive `agent` drives connection\npooling either way (`fetch` honours the injected `dispatcher`), and both read\nredirects manually (on Node, `fetch` with `redirect: \"manual\"` returns a\n*readable* redirect, not an opaque one). `test/transport-parity.test.js` runs\nthe same assertions through both engines to keep it that way.\n\n### Forcing a transport\n\n`transport` overrides the env-driven default:\n\n- `\"undici\"` — always use `undici.request` (e.g. a perf test that runs under\n  `NODE_ENV=test` but shouldn't switch engines; or if you mock with `MockAgent`).\n- `\"fetch\"` — always use global `fetch` (e.g. nock in a non-`test` env).\n- a function `(url, init) => Promise<{ statusCode, headers, body }>` — a custom\n  transport, used verbatim.\n\n## Test\n\n```sh\nnpm test\n```\n\nMocha + Chai. Most behavior is exercised against undici's `MockAgent` (injected as\n`agent` — honoured by both transports); a dedicated suite exercises the default\nfetch transport against `nock`; timeout and keepAlive connection-reuse are\nexercised against a real localhost server.\n","readmeFilename":"README.md"}