UNPKG

@anthropic-ai/sdk

Version:
174 lines 7.73 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isFetchOriginError = isFetchOriginError; exports.isRetryableError = isRetryableError; exports.wrapFetchWithMiddleware = wrapFetchWithMiddleware; exports.applyMiddleware = applyMiddleware; const errors_1 = require("../internal/errors.js"); const parse_1 = require("../internal/parse.js"); const log_1 = require("../internal/utils/log.js"); const error_1 = require("./error.js"); const streaming_1 = require("./streaming.js"); /** * Errors thrown by the underlying `fetch`, as opposed to by a middleware. * * Tracked so the client can apply its connection-error retry policy to * transport failures while letting errors thrown by middleware propagate to * the caller untouched. */ const fetchOriginErrors = new WeakSet(); /** Whether `err` was thrown by the underlying `fetch` rather than by a middleware. */ function isFetchOriginError(err) { return typeof err === 'object' && err !== null && fetchOriginErrors.has(err); } /** * Whether an error thrown by middleware should stay on the SDK's * connection-error retry policy: fetch-origin, abort, `APIConnectionError`, or * `RetryableError` — checked through the error's `cause` chain. */ function isRetryableError(err) { const seen = new Set(); // guard against `cause` cycles while (typeof err === 'object' && err !== null && !seen.has(err)) { seen.add(err); if (isFetchOriginError(err) || (0, errors_1.isAbortError)(err) || err instanceof error_1.APIConnectionError || err instanceof error_1.RetryableError) { return true; } err = err.cause; } return false; } /** * Wraps `fetchFn` so each call runs through `middleware`, keeping the same * call signature as `fetch` itself. * * With no middleware, calls are passed straight through to `fetchFn`. * Otherwise the arguments are normalized into an {@link APIRequest} (headers * coerced to a `Headers` instance, URL stringified) before entering the * chain. The chain is composed per call, so mutations of a `middleware` * array are picked up by later requests. * * `options` — the SDK request options behind this call, when there are any — * is surfaced to middleware as `ctx.options` and drives `ctx.parse`. * * `client` supplies `ctx.logger` (the client's level-filtered logger); * without it, `ctx.logger` falls back to the client defaults: `console`, * filtered to `ANTHROPIC_LOG` or `'warn'`. */ function wrapFetchWithMiddleware(fetchFn, middleware, options, client) { return async (url, init = {}) => { if (middleware.length === 0) { // use undefined this binding; fetch errors if bound to something else in browser/cloudflare return fetchFn.call(undefined, url, init); } const headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers); const response = await applyMiddleware(fetchFn, middleware, options, client)({ ...init, headers, url: typeof url === 'string' ? url : url instanceof URL ? url.href : url.url, }); // Catch a footgun before the client tries to read the body itself and // fails with a confusing low-level stream error. if (response.bodyUsed || response.body?.locked) { throw new error_1.AnthropicError('middleware consumed the response body; use response.clone() to inspect it, ' + 'or return new Response(body, response) to consume and replace it'); } return response; }; } /** * Creates the {@link MiddlewareContext} shared by every middleware in one chain. */ function createMiddlewareContext(options, client) { // Keyed on the Response so each `next()` call's response (e.g. with custom // retries, or a middleware swapping in a replacement) parses independently, // while several middleware parsing the same response share a single read. const cache = new WeakMap(); return { options, // Resolved per chain, so changes to the client's `logLevel`/`logger` // apply to subsequent requests. logger: client ? (0, log_1.loggerFor)(client) : (0, log_1.defaultLogger)(), parse(response) { // Streams are single-consumer, so caching one would hand later callers // an already-consumed stream; every call gets a fresh clone-backed one. if (options?.stream && response.ok) { return parseMiddlewareResponse(response, options); } let parsed = cache.get(response); if (!parsed) { parsed = parseMiddlewareResponse(response, options); cache.set(response, parsed); } return parsed; }, }; } /** * Mirrors the client's own response parsing (`defaultParseResponse` in * `internal/parse.ts`), reading through a clone so the body stays available * to the rest of the chain and the client itself. */ async function parseMiddlewareResponse(response, options) { if (response.bodyUsed || response.body?.locked) { throw new error_1.AnthropicError('cannot ctx.parse() a response whose body was already consumed; ' + 'call ctx.parse() instead of reading the body, or read via response.clone()'); } // Error responses parse as JSON/text below — the SDK only stream-parses // successful responses, and middleware typically wants the error body. if (options?.stream && response.ok) { // A fresh controller rather than the request's own: aborting (or // `break`ing out of) the middleware's stream must not cancel the // in-flight request the client is still reading. return streaming_1.Stream.fromSSEResponse(response.clone(), new AbortController()); } // fetch refuses to read the body when the status code is 204. if (response.status === 204) { return null; } if (options?.__binaryResponse) { return response; } const contentType = response.headers.get('content-type'); const mediaType = contentType?.split(';')[0]?.trim(); const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json'); if (isJSON) { if (response.headers.get('content-length') === '0') { // if there is no content we can't do anything return undefined; } return (0, parse_1.addRequestID)(await response.clone().json(), response); } return await response.clone().text(); } /** * Composes `middleware` around `fetchFn` and returns the entry point of the chain. */ function applyMiddleware(fetchFn, middleware, options, client) { // use undefined this binding; fetch errors if bound to something else in browser/cloudflare let next = async ({ url, ...init }) => { try { return await fetchFn.call(undefined, url, init); } catch (err) { // Brand the error as fetch-origin, normalizing with `castToError` first since a // WeakSet can't hold primitives and the brand must be on the same object the // client's own `castToError` will later pass through. const error = (0, errors_1.castToError)(err); fetchOriginErrors.add(error); throw error; } }; const ctx = createMiddlewareContext(options, client); for (let i = middleware.length - 1; i >= 0; i--) { const mw = middleware[i]; const nextInner = next; next = async (request) => mw(request, nextInner, ctx); } return next; } //# sourceMappingURL=middleware.js.map