Finish axios migration via shared native HTTP helper

Replace all in-scope axios callsites with a new helpers/http.js wrapper over native fetch, including JSON/text GET, JSON POST, HEAD checks, and transient 5xx retry behavior; update all browser, build, script, and proxy API clients to use it; add focused unit tests; and remove axios from package dependencies.

Constraint: Preserve API/build and deployment behavior while lowering transport surface area.

Rejected: inline fetch replacements at each callsite | rejected to avoid inconsistent error/retry semantics.

Confidence: high

Scope-risk: moderate

Directive: Keep helper in place as the transport boundary and update tests when changing request semantics.

Tested: pnpm run -s typecheck, pnpm -s run test-prebuild, pnpm -s run test, pnpm -s run test:browser, pnpm -s run netlify-build, smoke GETs on /apple-silicon-app-test and /apple-silicon-app-test/?version=2

Not-tested: branch/netlify deployment health in CI pipeline after merge
This commit is contained in:
ThatGuySam 2026-04-06 12:09:16 -05:00
parent d39a2a1d6c
commit d45b587434
25 changed files with 824 additions and 267 deletions

View file

@ -6,89 +6,63 @@ import {
vi
} from 'vitest'
import axios from 'axios'
import fs from 'fs-extra'
import {
fetchJsonWithRetries,
shouldRetryError
loadSitemapEndpoints
} from '~/helpers/pagefind/load-sitemap-endpoints'
import { getJson } from '~/helpers/http.js'
vi.mock( 'axios', () => {
vi.mock( 'fs-extra', () => {
return {
default: {
get: vi.fn()
pathExists: vi.fn(),
readJson: vi.fn()
}
}
} )
describe( 'load sitemap endpoints helper', () => {
vi.mock( '~/helpers/http.js', () => {
return {
getJson: vi.fn(),
shouldRetryError: vi.fn()
}
} )
describe( 'load sitemap endpoints', () => {
beforeEach( () => {
vi.mocked( axios.get ).mockReset()
vi.mocked( fs.pathExists ).mockReset()
vi.mocked( fs.readJson ).mockReset()
vi.mocked( getJson ).mockReset()
} )
it( 'retries transient 5xx errors and eventually resolves', async () => {
const axiosGet = vi.mocked( axios.get )
axiosGet
.mockRejectedValueOnce({
response: {
status: 502
}
})
.mockResolvedValueOnce({
data: {
ok: true
}
} )
const data = await fetchJsonWithRetries( 'https://api.doesitarm.com/sitemap-endpoints.json', {
attempts: 2,
delayMs: 0
it( 'reads the local sitemap-endpoints file when it exists', async () => {
vi.mocked( fs.pathExists ).mockResolvedValueOnce( true )
vi.mocked( fs.readJson ).mockResolvedValueOnce({
endpoints: [ '/api/app/spotify.json' ]
} )
expect( data ).toEqual({
ok: true
} )
expect( axiosGet ).toHaveBeenCalledTimes( 2 )
} )
it( 'does not retry non-5xx errors', async () => {
const axiosGet = vi.mocked( axios.get )
axiosGet.mockRejectedValueOnce({
response: {
status: 404
}
await expect( loadSitemapEndpoints() ).resolves.toEqual({
endpoints: [ '/api/app/spotify.json' ]
})
await expect( fetchJsonWithRetries( 'https://api.doesitarm.com/sitemap-endpoints.json', {
expect( getJson ).not.toHaveBeenCalled()
} )
it( 'falls back to the remote sitemap-endpoints JSON when the local file is missing', async () => {
vi.mocked( fs.pathExists ).mockResolvedValueOnce( false )
vi.mocked( getJson ).mockResolvedValueOnce({
endpoints: [ '/api/app/electron-framework.json' ]
} )
process.env.PUBLIC_API_DOMAIN = 'https://api.doesitarm.com'
await expect( loadSitemapEndpoints() ).resolves.toEqual({
endpoints: [ '/api/app/electron-framework.json' ]
} )
expect( getJson ).toHaveBeenCalledWith( 'https://api.doesitarm.com/sitemap-endpoints.json', {
attempts: 3,
delayMs: 0
} ) ).rejects.toEqual({
response: {
status: 404
}
delayMs: 1000
})
expect( axiosGet ).toHaveBeenCalledTimes( 1 )
} )
it( 'classifies retryable server errors', () => {
expect( shouldRetryError( {
response: {
status: 502
}
} ) ).toBe( true )
expect( shouldRetryError( {
response: {
status: 503
}
} ) ).toBe( true )
expect( shouldRetryError( {
response: {
status: 404
}
} ) ).toBe( false )
expect( shouldRetryError( new Error( 'network' ) ) ).toBe( false )
} )
} )