import type { FinalRequestOptions } from './request-options'; import { Stream } from '../core/streaming'; import { type BaseAnthropic } from '../client'; import { debugLogRequestDetails, loggerFor } from './utils/log'; import { releaseRequestSignal } from './request-signal'; import type { AbstractPage } from '../core/pagination'; export type APIResponseProps = { response: Response; options: FinalRequestOptions; controller: AbortController; requestLogID: string; retryOfRequestLogID: string | undefined; startTime: number; }; export async function defaultParseResponse( client: BaseAnthropic, props: APIResponseProps, ): Promise> { const { response, requestLogID, retryOfRequestLogID, startTime } = props; const body = await (async () => { if (props.options.stream) { loggerFor(client).debug('response', response.status, response.url, response.headers, response.body); // Note: there is an invariant here that isn't represented in the type system // that if you set `stream: true` the response type must also be `Stream` return Stream.fromSSEResponse(response, props.controller, client) as any; } // fetch refuses to read the body when the status code is 204. if (response.status === 204) { return null as T; } if (props.options.__binaryResponse) { return response as unknown as T; } const contentType = response.headers.get('content-type'); const mediaType = contentType?.split(';')[0]?.trim(); const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json'); if (isJSON) { const contentLength = response.headers.get('content-length'); if (contentLength === '0') { // if there is no content we can't do anything return undefined as T; } const json = await response.json(); return addResponseIDs(json as T, response); } const text = await response.text(); return text as unknown as T; })().finally(() => { // The body is settled (or parsing threw), so the caller-signal abort // listener has nothing left to cancel. Streams release in their own // teardown; a raw Response (`__binaryResponse`) keeps the listener so // aborting an in-flight download still works. if (!props.options.stream && !props.options.__binaryResponse) { releaseRequestSignal(props.controller); } }); debugLogRequestDetails(loggerFor(client), `[${requestLogID}] response parsed`, { retryOfRequestLogID, url: response.url, status: response.status, body, durationMs: Date.now() - startTime, }); return body; } export type WithRequestID = T extends Array | Response | AbstractPage ? T : T extends Record ? T & { _request_id?: string | null; _workspace_id?: string | null } : T; export function addResponseIDs(value: T, response: Response): WithRequestID { if (!value || typeof value !== 'object' || Array.isArray(value)) { return value as WithRequestID; } return Object.defineProperties(value, { _request_id: { value: response.headers.get('request-id'), enumerable: false }, _workspace_id: { value: response.headers.get('anthropic-workspace-id'), enumerable: false }, }) as WithRequestID; }