Motivation
The v13.11.6 → v13.11.7 hotfix cycle exposed how thin the project's axios usage is. The sandbox shipped a fake axios that was just a fetch wrapper silently dropping the params config — moving the built-ins to native fetch was a strict simplification, and the same logic applies to the wider apps/core codebase: we have one HttpService Nest wrapper plus eight call sites, all using a small slice of axios.
ofetch (unjs/ofetch) is the natural replacement: native fetch semantics, built-in retry, responseType parsing, FetchError with error.status / error.data, undici Agent for the dev self-signed cert escape hatch.
Scope
- In:
apps/core/src/**
- Out:
packages/api-client/adaptors/axios.ts and its tests — that adapter is a public api-client opt-in for downstream consumers, not internal use.
Surface decision
Keep the HttpService NestJS provider as the seam, swap its internals.
- Rename
axiosRef → fetch returning an ofetch $Fetch instance.
- Eight call sites change from
httpService.axiosRef.get(url, opts) to httpService.fetch(url, opts) (or .fetch(url, { method: 'POST', body })).
- Test mocks stay shape-compatible:
{ fetch: vi.fn() } replaces { axiosRef: { get } }.
- Drop
extend() and getAndCacheRequest() — no callers in the tree.
- Drop the
bindDebugVerboseInterceptor path — __debugLogger is never set anywhere, dead code.
Files affected
Production code (apps/core/src):
processors/helper/helper.http.service.ts — rewrite as ofetch wrapper
app.config.ts + app.config.test.ts — AXIOS_CONFIG becomes the ofetch defaults (timeout; dev TLS via undici Agent)
processors/helper/helper.bark.service.ts:40 — POST JSON
processors/helper/helper.image.service.ts:119 — GET arrayBuffer
modules/webhook/webhook.service.ts:187 — POST JSON
modules/cron-task/cron-business.service.ts:118,155 — POST JSON
modules/update/update-download.service.ts:85,189 — GET (retry path) + GET (download with progress)
modules/comment/comment-country.service.ts:85 — GET JSON
modules/link/link-avatar.service.ts:107 — GET arrayBuffer, no-redirect
Tests:
test/src/modules/update/update.service.spec.ts:143,168,203 — replace axiosRef.get mocks
test/src/modules/comment/comment-country.spec.ts:31,139 — replace axiosRef.get mocks
package.json:
- Remove
axios, axios-retry
- Add
ofetch
Behaviour-preserving notes
- Retry:
axios-retry with exponentialDelay + 5 retries → ofetch retry: 5 with retryDelay: (ctx) => 1000 * 2 ** attempt (capped at 10s). Keep the warn log on retry.
- Dev self-signed TLS: replace
https.Agent({ rejectUnauthorized: false }) with new Agent({ connect: { rejectUnauthorized: false } }) from undici, passed as dispatcher.
- Download progress (
update-download.service.ts): preserve the admin progress UX by reading responseType: 'stream' (ReadableStream<Uint8Array>), tracking accumulated bytes vs Content-Length, and calling the existing pushProgress callback. ~30 LOC.
- GitHub 403 detection (
update-download.service.ts:98): axios.isAxiosError(e) && e.response?.status === 403 → e instanceof FetchError && e.status === 403.
- No-redirect (
link-avatar.service.ts): maxRedirects: 0 → redirect: 'manual' (native fetch option).
- Default UA header (
MX-Space/${PKG.version}) and 10s timeout: set in the shared ofetch.create({...}) defaults.
Out of scope
- The sandbox runtime — already migrated off the fake axios stub in v13.11.7.
- The
api-client/adaptors/axios.ts public adapter — stays.
- Any behaviour change beyond what is strictly necessary to swap the HTTP client.
Risk
Single-version refactor on master. Touches the egress path used by: friend-link avatar internalisation, webhook delivery, cron-driven HTTP probes, image proxy, IP geolocation, update download, Bark push. Worth landing as its own minor (v13.12.0) with manual smoke of at least: a webhook fire, an update check, and a friend-link avatar fetch.
Motivation
The v13.11.6 → v13.11.7 hotfix cycle exposed how thin the project's axios usage is. The sandbox shipped a fake
axiosthat was just afetchwrapper silently dropping theparamsconfig — moving the built-ins to nativefetchwas a strict simplification, and the same logic applies to the widerapps/corecodebase: we have oneHttpServiceNest wrapper plus eight call sites, all using a small slice of axios.ofetch (
unjs/ofetch) is the natural replacement: nativefetchsemantics, built-in retry,responseTypeparsing,FetchErrorwitherror.status/error.data, undiciAgentfor the dev self-signed cert escape hatch.Scope
apps/core/src/**packages/api-client/adaptors/axios.tsand its tests — that adapter is a public api-client opt-in for downstream consumers, not internal use.Surface decision
Keep the
HttpServiceNestJS provider as the seam, swap its internals.axiosRef→fetchreturning an ofetch$Fetchinstance.httpService.axiosRef.get(url, opts)tohttpService.fetch(url, opts)(or.fetch(url, { method: 'POST', body })).{ fetch: vi.fn() }replaces{ axiosRef: { get } }.extend()andgetAndCacheRequest()— no callers in the tree.bindDebugVerboseInterceptorpath —__debugLoggeris never set anywhere, dead code.Files affected
Production code (apps/core/src):
processors/helper/helper.http.service.ts— rewrite as ofetch wrapperapp.config.ts+app.config.test.ts—AXIOS_CONFIGbecomes the ofetch defaults (timeout; dev TLS via undiciAgent)processors/helper/helper.bark.service.ts:40— POST JSONprocessors/helper/helper.image.service.ts:119— GET arrayBuffermodules/webhook/webhook.service.ts:187— POST JSONmodules/cron-task/cron-business.service.ts:118,155— POST JSONmodules/update/update-download.service.ts:85,189— GET (retry path) + GET (download with progress)modules/comment/comment-country.service.ts:85— GET JSONmodules/link/link-avatar.service.ts:107— GET arrayBuffer, no-redirectTests:
test/src/modules/update/update.service.spec.ts:143,168,203— replaceaxiosRef.getmockstest/src/modules/comment/comment-country.spec.ts:31,139— replaceaxiosRef.getmockspackage.json:axios,axios-retryofetchBehaviour-preserving notes
axios-retrywithexponentialDelay+ 5 retries → ofetchretry: 5withretryDelay: (ctx) => 1000 * 2 ** attempt(capped at 10s). Keep the warn log on retry.https.Agent({ rejectUnauthorized: false })withnew Agent({ connect: { rejectUnauthorized: false } })fromundici, passed asdispatcher.update-download.service.ts): preserve the admin progress UX by readingresponseType: 'stream'(ReadableStream<Uint8Array>), tracking accumulated bytes vsContent-Length, and calling the existingpushProgresscallback. ~30 LOC.update-download.service.ts:98):axios.isAxiosError(e) && e.response?.status === 403→e instanceof FetchError && e.status === 403.link-avatar.service.ts):maxRedirects: 0→redirect: 'manual'(native fetch option).MX-Space/${PKG.version}) and 10s timeout: set in the sharedofetch.create({...})defaults.Out of scope
api-client/adaptors/axios.tspublic adapter — stays.Risk
Single-version refactor on master. Touches the egress path used by: friend-link avatar internalisation, webhook delivery, cron-driven HTTP probes, image proxy, IP geolocation, update download, Bark push. Worth landing as its own minor (v13.12.0) with manual smoke of at least: a webhook fire, an update check, and a friend-link avatar fetch.