Skip to content
← Public packages

@kody/api-research

Research third-party APIs: registry search, provider discovery, OpenAPI summarize, and client scaffold.

src/fetch-spec-lib.ts

88 lines · 2.3 KB · TypeScript
import {
	BoundedBodyTooLargeError,
	readBoundedBody,
} from './bounded-body.ts'
import { getErrorMessage } from './error-message.ts'
import { assertHttpsUrl } from './https.ts'

export const defaultOpenApiSpecMaxBytes = 4_000_000
export const defaultOpenApiSpecTimeoutMs = 30_000
const maxRedirectHops = 5

function isRedirectStatus(status: number): boolean {
	return status >= 300 && status < 400
}

export async function fetchOpenApiSpecText(input: {
	specUrl: string
	maxBytes?: number
	timeoutMs?: number
	fetchImpl?: typeof fetch
}): Promise<string> {
	let url = assertHttpsUrl(input.specUrl, 'OpenAPI spec URL')
	const maxBytes = input.maxBytes ?? defaultOpenApiSpecMaxBytes
	const timeoutMs = input.timeoutMs ?? defaultOpenApiSpecTimeoutMs
	const fetchImpl = input.fetchImpl ?? fetch

	let response: Response
	let redirected = false
	try {
		for (let hop = 0; ; hop += 1) {
			response = await fetchImpl(url.toString(), {
				headers: {
					Accept: 'application/json, application/yaml, text/yaml, */*',
				},
				redirect: 'manual',
				signal: AbortSignal.timeout(timeoutMs),
			})

			if (!isRedirectStatus(response.status)) {
				break
			}

			void response.body?.cancel().catch(() => {})

			if (hop >= maxRedirectHops) {
				throw new Error(
					`OpenAPI spec fetch failed: too many redirects (max ${maxRedirectHops})`,
				)
			}

			const location = response.headers.get('location')
			if (location == null || location.trim().length === 0) {
				throw new Error(
					'OpenAPI spec fetch failed: redirect response missing Location header',
				)
			}

			const nextUrl = new URL(location, url)
			url = assertHttpsUrl(nextUrl.toString(), 'OpenAPI spec URL')
			redirected = true
		}
	} catch (cause) {
		if (
			cause instanceof Error &&
			cause.message.startsWith('OpenAPI spec fetch failed:')
		) {
			throw cause
		}
		throw new Error(`OpenAPI spec fetch failed: ${getErrorMessage(cause)}`)
	}

	if (!response.ok) {
		throw new Error(
			redirected
				? 'OpenAPI spec fetch failed: redirect destination did not return the spec'
				: `OpenAPI spec fetch failed: HTTP ${response.status} for ${input.specUrl}`,
		)
	}

	try {
		return await readBoundedBody(response, maxBytes)
	} catch (cause) {
		if (cause instanceof BoundedBodyTooLargeError) {
			throw new Error(`OpenAPI spec fetch failed: ${cause.message}`)
		}
		throw cause
	}
}