mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
fix(ata): fall back to the npm proxy when the CDN request fails outright (#10630)
* fix(ata): fall back to the npm proxy when the CDN request fails outright * fix(ata): surface proxy failures and guard the body read too * docs(ata): state the proxy catch's constraint, not its history * fix(ata): log a failed proxy d.ts fetch, which callers discard
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getDTSFileForModuleWithVersion, getNPMVersionsForModule } from './apis'
|
||||
|
||||
/** jsdelivr unreachable (blocked egress, DNS failure), backend proxy answering. */
|
||||
function mockUnreachableJsdelivr(proxyBody: string) {
|
||||
return vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input)
|
||||
if (url.includes('jsdelivr')) throw new TypeError('Failed to fetch')
|
||||
if (url.includes('/npm_proxy/')) return new Response(proxyBody, { status: 200 })
|
||||
throw new Error(`unexpected request to ${url}`)
|
||||
})
|
||||
}
|
||||
|
||||
describe('ATA backend proxy fallback', () => {
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
it('falls back to the proxy when the jsdelivr request rejects', async () => {
|
||||
workspaceStore.set('test-workspace')
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
mockUnreachableJsdelivr('{"tags":{"latest":"1.0.0"},"versions":["1.0.0"]}')
|
||||
)
|
||||
|
||||
const versions = await getNPMVersionsForModule('lodash', { usage: 0 })
|
||||
|
||||
expect(versions).not.toBeInstanceOf(Error)
|
||||
expect((versions as { versions: string[] }).versions).toEqual(['1.0.0'])
|
||||
})
|
||||
|
||||
it('falls back to the proxy for a d.ts when the jsdelivr request rejects', async () => {
|
||||
workspaceStore.set('test-workspace')
|
||||
vi.stubGlobal('fetch', mockUnreachableJsdelivr('declare const x: number'))
|
||||
|
||||
const dts = await getDTSFileForModuleWithVersion('lodash', '1.0.0', '/index.d.ts')
|
||||
|
||||
expect(dts).toBe('declare const x: number')
|
||||
})
|
||||
})
|
||||
@@ -25,7 +25,9 @@ const backendProxyApi = async <T>(endpoint: string, resLimit: ResLimit): Promise
|
||||
const baseUrl = getBackendProxyUrl()
|
||||
const url = `${baseUrl}${endpoint}`
|
||||
|
||||
return limit(() =>
|
||||
// `await`, not a bare `return`: an async function adopts a returned promise after
|
||||
// leaving the try block, so a rejection would escape the catch below.
|
||||
return await limit(() =>
|
||||
fetch(url, { credentials: 'include' }).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.text().then((text) => {
|
||||
@@ -39,7 +41,10 @@ const backendProxyApi = async <T>(endpoint: string, resLimit: ResLimit): Promise
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
return new Error('Backend proxy not available')
|
||||
// Keep the cause: where the proxy is the only reachable source, this is the sole
|
||||
// report of a failed acquisition, since callers only test the result for `Error`.
|
||||
console.warn(`Backend proxy request to ${endpoint} failed`, e)
|
||||
return new Error(`Backend proxy not available: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,23 +136,37 @@ export const getDTSFileForModuleWithVersion = async (
|
||||
) => {
|
||||
// file comes with a prefix /
|
||||
const url = `https://cdn.jsdelivr.net/npm/${moduleName}@${version}${file}`
|
||||
const res = await limit(() => fetch(url))
|
||||
if (res.ok) {
|
||||
return res.text()
|
||||
} else {
|
||||
// Try backend proxy
|
||||
console.log('jsdelivr failed for file', moduleName, version, file, 'trying backend proxy')
|
||||
try {
|
||||
const baseUrl = getBackendProxyUrl()
|
||||
const proxyUrl = `${baseUrl}/file/${encodeURIComponent(moduleName)}/${encodeURIComponent(version)}${file}`
|
||||
const proxyRes = await limit(() => fetch(proxyUrl, { credentials: 'include' }))
|
||||
if (proxyRes.ok) {
|
||||
return proxyRes.text()
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Backend proxy failed for file', e)
|
||||
}
|
||||
return new Error('OK')
|
||||
const res = await text(url)
|
||||
if (typeof res === 'string') {
|
||||
return res
|
||||
}
|
||||
|
||||
// Try backend proxy
|
||||
console.log('jsdelivr failed for file', moduleName, version, file, 'trying backend proxy')
|
||||
try {
|
||||
const baseUrl = getBackendProxyUrl()
|
||||
const proxyUrl = `${baseUrl}/file/${encodeURIComponent(moduleName)}/${encodeURIComponent(version)}${file}`
|
||||
const proxied = await text(proxyUrl, { credentials: 'include' })
|
||||
// The callers in ata/index.ts log a fixed message and drop the value, so a cause
|
||||
// left inside the returned `Error` is a cause nothing ever prints.
|
||||
if (proxied instanceof Error) console.warn('Backend proxy failed for file', proxied)
|
||||
return proxied
|
||||
} catch (e) {
|
||||
console.warn('Backend proxy failed for file', e)
|
||||
return new Error(`Backend proxy not available: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reading the body can fail as readily as connecting, and both have to surface as a value
|
||||
* rather than a rejection for the caller's fallback to run.
|
||||
*/
|
||||
async function text(url: string, init?: RequestInit): Promise<string | Error> {
|
||||
try {
|
||||
const res = await limit(() => fetch(url, init))
|
||||
return res.ok ? await res.text() : new Error(`${res.status} for ${url}`)
|
||||
} catch (e) {
|
||||
return new Error(`Request to ${url} failed: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,21 +185,26 @@ function api<T>(url: string, resLimit: ResLimit, init?: RequestInit): Promise<T
|
||||
console.warn(
|
||||
`Exceeded limit of types downloaded for the needs of the assistant fetching: ${url}, ${resLimit.usage}`
|
||||
)
|
||||
return new Promise(() => new Error('Exceeded limit of 100MB of data downloaded.'))
|
||||
return Promise.resolve(new Error('Exceeded limit of 100MB of data downloaded.'))
|
||||
}
|
||||
|
||||
// Every caller decides what to do next by testing the resolved value for `Error`, so a
|
||||
// rejection here is not an alternative signal: it skips the backend-proxy fallback and
|
||||
// propagates out of type acquisition entirely.
|
||||
return limit(() =>
|
||||
fetch(url, init).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.text().then((text) => {
|
||||
resLimit.usage += text.length
|
||||
console.log('resLimit', url, resLimit.usage)
|
||||
fetch(url, init)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.text().then((text) => {
|
||||
resLimit.usage += text.length
|
||||
console.log('resLimit', url, resLimit.usage)
|
||||
|
||||
return JSON.parse(text) as T
|
||||
}) as Promise<T | Error>
|
||||
} else {
|
||||
return new Error('OK')
|
||||
}
|
||||
})
|
||||
return JSON.parse(text) as T
|
||||
}) as Promise<T | Error>
|
||||
} else {
|
||||
return new Error('OK')
|
||||
}
|
||||
})
|
||||
.catch((e) => new Error(`Request to ${url} failed: ${e}`))
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user