axios+telemetry cleanup
This commit is contained in:
125
utils/http.ts
125
utils/http.ts
@@ -2,7 +2,6 @@
|
||||
* HTTP utility constants and helpers
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import { OAUTH_BETA_HEADER } from '../constants/oauth.js'
|
||||
import {
|
||||
getAnthropicApiKey,
|
||||
@@ -31,7 +30,7 @@ export function getUserAgent(): string {
|
||||
// so the read picks up the same setWorkload() value as getAttributionHeader.
|
||||
const workload = getWorkload()
|
||||
const workloadSuffix = workload ? `, workload/${workload}` : ''
|
||||
return `claude-cli/${MACRO.VERSION} (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})`
|
||||
return `claude-cli/0.1.0-alpha (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})`
|
||||
}
|
||||
|
||||
export function getMCPUserAgent(): string {
|
||||
@@ -46,7 +45,7 @@ export function getMCPUserAgent(): string {
|
||||
parts.push(`client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`)
|
||||
}
|
||||
const suffix = parts.length > 0 ? ` (${parts.join(', ')})` : ''
|
||||
return `claude-code/${MACRO.VERSION}${suffix}`
|
||||
return `claude-code/0.1.0-alpha${suffix}`
|
||||
}
|
||||
|
||||
// User-Agent for WebFetch requests to arbitrary sites. `Claude-User` is
|
||||
@@ -98,6 +97,118 @@ export function getAuthHeaders(): AuthHeaders {
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status?: number,
|
||||
public data?: any,
|
||||
public headers?: Record<string, string>,
|
||||
public code?: string,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'HttpError'
|
||||
}
|
||||
}
|
||||
|
||||
export function isHttpError(error: unknown): error is HttpError {
|
||||
return error instanceof HttpError
|
||||
}
|
||||
|
||||
export type NativeRequestOptions = {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
|
||||
headers?: Record<string, string>
|
||||
body?: any
|
||||
timeout?: number
|
||||
signal?: AbortSignal
|
||||
responseType?: 'json' | 'arraybuffer' | 'text' | 'none'
|
||||
dispatcher?: any // undici.Dispatcher
|
||||
}
|
||||
|
||||
export async function nativeRequest<T>(
|
||||
url: string,
|
||||
options: NativeRequestOptions = {},
|
||||
): Promise<{ data: T; status: number; headers: Record<string, string> }> {
|
||||
const {
|
||||
method = 'GET',
|
||||
headers = {},
|
||||
body,
|
||||
timeout,
|
||||
responseType = 'json',
|
||||
dispatcher,
|
||||
} = options
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = timeout
|
||||
? setTimeout(() => controller.abort(), timeout)
|
||||
: null
|
||||
|
||||
try {
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
signal: options.signal || controller.signal,
|
||||
...(dispatcher ? { dispatcher } : {}),
|
||||
} as RequestInit
|
||||
|
||||
if (body) {
|
||||
if (
|
||||
body instanceof Buffer ||
|
||||
body instanceof Uint8Array ||
|
||||
body instanceof Blob ||
|
||||
body instanceof FormData
|
||||
) {
|
||||
fetchOptions.body = body as any
|
||||
} else {
|
||||
if (!fetchOptions.headers) fetchOptions.headers = {}
|
||||
;(fetchOptions.headers as any)['Content-Type'] = 'application/json'
|
||||
fetchOptions.body = JSON.stringify(body)
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOptions)
|
||||
|
||||
let responseData: any
|
||||
if (responseType === 'arraybuffer') {
|
||||
responseData = await response.arrayBuffer()
|
||||
} else if (responseType === 'text') {
|
||||
responseData = await response.text()
|
||||
} else if (responseType === 'none') {
|
||||
responseData = null
|
||||
} else {
|
||||
responseData = await response.json().catch(() => null)
|
||||
}
|
||||
|
||||
const responseHeaders: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
responseHeaders[key] = value
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new HttpError(
|
||||
`HTTP Error ${response.status}`,
|
||||
response.status,
|
||||
responseData,
|
||||
responseHeaders,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
data: responseData as T,
|
||||
status: response.status,
|
||||
headers: responseHeaders,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new HttpError('Request aborted/timeout', 408, null, {}, 'ECONNABORTED')
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper that handles OAuth 401 errors by force-refreshing the token and
|
||||
* retrying once. Addresses clock drift scenarios where the local expiration
|
||||
@@ -119,14 +230,14 @@ export async function withOAuth401Retry<T>(
|
||||
try {
|
||||
return await request()
|
||||
} catch (err) {
|
||||
if (!axios.isAxiosError(err)) throw err
|
||||
const status = err.response?.status
|
||||
if (!isHttpError(err)) throw err
|
||||
const status = err.status
|
||||
const isAuthError =
|
||||
status === 401 ||
|
||||
(opts?.also403Revoked &&
|
||||
status === 403 &&
|
||||
typeof err.response?.data === 'string' &&
|
||||
err.response.data.includes('OAuth token has been revoked'))
|
||||
typeof err.data === 'string' &&
|
||||
err.data.includes('OAuth token has been revoked'))
|
||||
if (!isAuthError) throw err
|
||||
const failedAccessToken = getClaudeAIOAuthTokens()?.accessToken
|
||||
if (!failedAccessToken) throw err
|
||||
|
||||
Reference in New Issue
Block a user