@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
106 lines (92 loc) • 3.97 kB
text/typescript
import {Readable} from 'node:stream'
import type {FetchFunction} from 'get-it'
import {debug} from 'get-it/middleware'
import {createNodeFetch} from 'get-it/node'
import {createDebug} from 'obug'
import {name, version} from '../../package.json'
import type {EnvironmentOptions, LegacyMiddleware} from './request'
const log = createDebug('sanity:client')
function isNodeReadableStream(value: unknown): value is Readable {
if (typeof value !== 'object' || value === null) return false
if (!('pipe' in value)) return false
return typeof value.pipe === 'function'
}
// One undici-backed fetch per (proxy URL) so callers don't pay the cost of
// rebuilding a dispatcher on every request when they use the same proxy.
const proxyFetchCache = new Map<string, ReturnType<typeof createNodeFetch>>()
function getProxyFetch(proxyUrl: string): ReturnType<typeof createNodeFetch> {
const cached = proxyFetchCache.get(proxyUrl)
if (cached) return cached
const fetch = createNodeFetch({proxy: proxyUrl, connections: 30})
proxyFetchCache.set(proxyUrl, fetch)
return fetch
}
// The environment's default fetch, shared by all SSE connections that have no
// explicit proxy configured. Built lazily so clients that never use
// `listen()`/`live.events()` don't pay for the dispatcher.
let envDefaultFetch: ReturnType<typeof createNodeFetch> | undefined
/**
* Exposed via `EnvironmentOptions.resolveFetch` so EventSource connections
* use the same transport as regular requests instead of falling back to
* `globalThis.fetch`. Everything get-it's Node fetch provides then applies
* to SSE too: the undici dispatcher configuration, an explicit `proxy`
* config (pass the URL), and env-proxy support
* (`HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`, which Node's own global fetch does
* not read — that is opt-in via `NODE_USE_ENV_PROXY`, and not on our 22.12
* floor). Reached via the env rather than a direct import so `get-it/node`
* (and with it `undici`) stays out of the browser bundle.
*
* @internal
*/
function resolveFetch(proxyUrl?: string): FetchFunction {
if (typeof proxyUrl === 'string') {
return getProxyFetch(proxyUrl)
}
envDefaultFetch ??= createNodeFetch()
return envDefaultFetch
}
const middleware: LegacyMiddleware[] = [
debug({log: (message, ...args) => log(message, ...args), verbose: true}),
// Lineage is used for recursion control/tracing and can be passed either
// through the client config (arrives as `meta.lineage`, set by
// `requestOptions`) or through an environment variable. Not used in
// browser environments.
{
beforeRequest(opts) {
const configLineage = typeof opts.meta?.lineage === 'string' ? opts.meta.lineage : undefined
const lineage =
(typeof process !== 'undefined' && process.env.X_SANITY_LINEAGE) || configLineage
if (!lineage) return opts
const headers = new Headers(opts.headers)
headers.set('x-sanity-lineage', lineage)
return {...opts, headers}
},
},
// Asset uploads can pass a Node.js Readable stream (from
// `fs.createReadStream(...)`) as the body. The HTTP transport's body type
// guard only accepts Web streams, so we project Node streams here —
// undici's fetch accepts Web streams natively.
{
beforeRequest(opts) {
if (!isNodeReadableStream(opts.body)) return opts
return {...opts, body: Readable.toWeb(opts.body)}
},
},
]
/**
* Node-specific environment options used to wire up `createRequester`.
*
* - `User-Agent` defaults to `@sanity/client <version>`.
* - Falls back to get-it/node's default undici-backed fetch (which honours
* `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` env vars via undici's
* `EnvHttpProxyAgent`). Per-request proxy overrides are wired in via the
* wrapping middleware above.
*
* @internal
*/
const environment: EnvironmentOptions = {
headers: {'User-Agent': `${name} ${version}`},
middleware,
resolveFetch,
}
export default environment