push neon reactor
This commit is contained in:
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
/**
|
||||
* Parse a CAE (Continuous Access Evaluation) claims challenge from a
|
||||
* WWW-Authenticate header.
|
||||
*
|
||||
* Returns the decoded claims JSON string when a Bearer challenge with
|
||||
* `error="insufficient_claims"` is found, or `undefined` otherwise.
|
||||
*/
|
||||
export declare function parseCaeChallenge(wwwAuthenticate: string | undefined): string | undefined;
|
||||
//# sourceMappingURL=CaeUtils.d.ts.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"CaeUtils.d.ts","sourceRoot":"","sources":["../../src/http/CaeUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CA2BzF"}
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
/**
|
||||
* Parse a CAE (Continuous Access Evaluation) claims challenge from a
|
||||
* WWW-Authenticate header.
|
||||
*
|
||||
* Returns the decoded claims JSON string when a Bearer challenge with
|
||||
* `error="insufficient_claims"` is found, or `undefined` otherwise.
|
||||
*/
|
||||
export function parseCaeChallenge(wwwAuthenticate) {
|
||||
if (!wwwAuthenticate) {
|
||||
return undefined;
|
||||
}
|
||||
// Split on "Bearer " to handle multiple challenges in one header.
|
||||
const challenges = wwwAuthenticate.split('Bearer ');
|
||||
for (const challenge of challenges) {
|
||||
if (!challenge.includes('error="insufficient_claims"')) {
|
||||
continue;
|
||||
}
|
||||
const claimsMatch = challenge.match(/claims="([^"]+)"/);
|
||||
if (!claimsMatch) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return atob(claimsMatch[1]);
|
||||
}
|
||||
catch {
|
||||
// Malformed base64 — treat as no challenge.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
//# sourceMappingURL=CaeUtils.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"CaeUtils.js","sourceRoot":"","sources":["../../src/http/CaeUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,eAAmC;IACnE,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,kEAAkE;IAClE,MAAM,UAAU,GAAG,eAAe,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAEpD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,6BAA6B,CAAC,EAAE,CAAC;YACvD,SAAS;QACX,CAAC;QAED,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACxD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,SAAS;QACX,CAAC;QAED,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,4CAA4C;YAC5C,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
||||
Generated
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
import type { ServiceEndpoint } from '../services/ServiceEndpoint.js';
|
||||
import type { ILogger } from '../telemetry/ILogger.js';
|
||||
import type { IAuthenticationProvider } from '../types/AuthenticationTypes.js';
|
||||
import type { HttpRequestConfig, HttpResponse, RequestInterceptor, ResponseInterceptor } from './HttpTypes.js';
|
||||
/** Options for creating an HttpClient instance. */
|
||||
export interface HttpClientOptions {
|
||||
/** Authentication provider for automatic token acquisition. */
|
||||
authProvider?: IAuthenticationProvider;
|
||||
/** Default headers included in every request. */
|
||||
defaultHeaders?: Record<string, string>;
|
||||
/** Default timeout in milliseconds for all requests. */
|
||||
defaultTimeoutMs?: number;
|
||||
/** Custom fetch implementation (defaults to globalThis.fetch). */
|
||||
fetch?: typeof globalThis.fetch;
|
||||
/** Logger for automatic HTTP request telemetry. */
|
||||
logger?: ILogger;
|
||||
}
|
||||
/** Generic HTTP client with auth, service name routing, interceptors, and CAE support. */
|
||||
export declare class HttpClient {
|
||||
private readonly defaultHeaders;
|
||||
private readonly defaultTimeoutMs;
|
||||
private readonly fetchFn;
|
||||
private readonly requestInterceptors;
|
||||
private readonly responseInterceptors;
|
||||
private readonly authProvider?;
|
||||
private readonly logger?;
|
||||
private readonly endpoints;
|
||||
constructor(options?: HttpClientOptions);
|
||||
/** Register a named service endpoint for service-name URL resolution. Returns the registered name. */
|
||||
registerEndpoint(name: string, endpoint: ServiceEndpoint): string;
|
||||
/** Add a request interceptor. Returns a function to remove it. */
|
||||
addRequestInterceptor(interceptor: RequestInterceptor): () => void;
|
||||
/** Add a response interceptor. Returns a function to remove it. */
|
||||
addResponseInterceptor(interceptor: ResponseInterceptor): () => void;
|
||||
/** Send a GET request. */
|
||||
get<T = unknown>(url: string, config?: Omit<HttpRequestConfig, 'url' | 'method'>): Promise<HttpResponse<T>>;
|
||||
/** Send a POST request. */
|
||||
post<T = unknown>(url: string, body?: unknown, config?: Omit<HttpRequestConfig, 'url' | 'method' | 'body'>): Promise<HttpResponse<T>>;
|
||||
/** Send a PUT request. */
|
||||
put<T = unknown>(url: string, body?: unknown, config?: Omit<HttpRequestConfig, 'url' | 'method' | 'body'>): Promise<HttpResponse<T>>;
|
||||
/** Send a PATCH request. */
|
||||
patch<T = unknown>(url: string, body?: unknown, config?: Omit<HttpRequestConfig, 'url' | 'method' | 'body'>): Promise<HttpResponse<T>>;
|
||||
/** Send a DELETE request. */
|
||||
delete<T = unknown>(url: string, config?: Omit<HttpRequestConfig, 'url' | 'method'>): Promise<HttpResponse<T>>;
|
||||
/** Send an HTTP request with the given configuration. */
|
||||
request<T = unknown>(config: HttpRequestConfig): Promise<HttpResponse<T>>;
|
||||
/** Execute fetch with CAE retry logic. Returns the response and whether a retry occurred. */
|
||||
private executeWithCaeRetry;
|
||||
/**
|
||||
* Execute the raw fetch, parse the response, throw HttpError on !ok,
|
||||
* and apply response interceptors.
|
||||
*/
|
||||
private executeFetch;
|
||||
/**
|
||||
* Resolve a URL to its final form and determine the auth resource.
|
||||
*
|
||||
* 1. Absolute URL — used as-is; resource resolved by matching registered
|
||||
* endpoint baseUrls (or undefined if no match).
|
||||
* 2. Service name prefix — first path segment matches a registered endpoint.
|
||||
* 3. Unresolved — passed through as-is; no resource.
|
||||
*/
|
||||
private resolveUrl;
|
||||
private composeAbortSignals;
|
||||
/** Assemble and emit the HTTP telemetry event. */
|
||||
private logRequest;
|
||||
}
|
||||
//# sourceMappingURL=HttpClient.d.ts.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"HttpClient.d.ts","sourceRoot":"","sources":["../../src/http/HttpClient.ts"],"names":[],"mappings":"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"}
|
||||
Generated
Vendored
+339
@@ -0,0 +1,339 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
import { parseCaeChallenge } from './CaeUtils.js';
|
||||
import { HttpError } from './HttpTypes.js';
|
||||
/** Generic HTTP client with auth, service name routing, interceptors, and CAE support. */
|
||||
export class HttpClient {
|
||||
defaultHeaders;
|
||||
defaultTimeoutMs;
|
||||
fetchFn;
|
||||
requestInterceptors = [];
|
||||
responseInterceptors = [];
|
||||
authProvider;
|
||||
logger;
|
||||
endpoints = new Map();
|
||||
constructor(options = {}) {
|
||||
this.defaultHeaders = options.defaultHeaders ?? {};
|
||||
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 30_000;
|
||||
this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
||||
this.authProvider = options.authProvider;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
/** Register a named service endpoint for service-name URL resolution. Returns the registered name. */
|
||||
registerEndpoint(name, endpoint) {
|
||||
this.endpoints.set(name, endpoint);
|
||||
return name;
|
||||
}
|
||||
/** Add a request interceptor. Returns a function to remove it. */
|
||||
addRequestInterceptor(interceptor) {
|
||||
this.requestInterceptors.push(interceptor);
|
||||
return () => {
|
||||
const index = this.requestInterceptors.indexOf(interceptor);
|
||||
if (index !== -1) {
|
||||
this.requestInterceptors.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
/** Add a response interceptor. Returns a function to remove it. */
|
||||
addResponseInterceptor(interceptor) {
|
||||
this.responseInterceptors.push(interceptor);
|
||||
return () => {
|
||||
const index = this.responseInterceptors.indexOf(interceptor);
|
||||
if (index !== -1) {
|
||||
this.responseInterceptors.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
/** Send a GET request. */
|
||||
async get(url, config) {
|
||||
return this.request({ ...config, url, method: 'GET' });
|
||||
}
|
||||
/** Send a POST request. */
|
||||
async post(url, body, config) {
|
||||
return this.request({ ...config, url, method: 'POST', body });
|
||||
}
|
||||
/** Send a PUT request. */
|
||||
async put(url, body, config) {
|
||||
return this.request({ ...config, url, method: 'PUT', body });
|
||||
}
|
||||
/** Send a PATCH request. */
|
||||
async patch(url, body, config) {
|
||||
return this.request({ ...config, url, method: 'PATCH', body });
|
||||
}
|
||||
/** Send a DELETE request. */
|
||||
async delete(url, config) {
|
||||
return this.request({ ...config, url, method: 'DELETE' });
|
||||
}
|
||||
/** Send an HTTP request with the given configuration. */
|
||||
async request(config) {
|
||||
let processedConfig = { ...config };
|
||||
// Apply request interceptors
|
||||
for (const interceptor of this.requestInterceptors) {
|
||||
processedConfig = await interceptor(processedConfig);
|
||||
}
|
||||
// Resolve service-name URLs and determine auth resource + endpoint metadata
|
||||
const { resolvedUrl, resource, endpointHeaders, extractTelemetryProperties } = this.resolveUrl(processedConfig.url);
|
||||
processedConfig = { ...processedConfig, url: resolvedUrl };
|
||||
// Resolve endpoint headers (evaluating functions) and merge under per-request headers
|
||||
let resolvedEndpointHeaders;
|
||||
if (endpointHeaders) {
|
||||
resolvedEndpointHeaders = {};
|
||||
for (const [name, value] of Object.entries(endpointHeaders)) {
|
||||
resolvedEndpointHeaders[name] = typeof value === 'function' ? value() : value;
|
||||
}
|
||||
processedConfig = {
|
||||
...processedConfig,
|
||||
headers: { ...resolvedEndpointHeaders, ...processedConfig.headers },
|
||||
};
|
||||
}
|
||||
// Stamp auth token if provider is configured and no Authorization header is already set
|
||||
if (this.authProvider && resource && !processedConfig.headers?.['Authorization']) {
|
||||
const token = await this.authProvider.getToken({ resource });
|
||||
processedConfig = {
|
||||
...processedConfig,
|
||||
headers: { ...processedConfig.headers, Authorization: `Bearer ${token.accessToken}` },
|
||||
};
|
||||
}
|
||||
// Execute with timing and telemetry
|
||||
const startTime = this.logger ? performance.now() : 0;
|
||||
try {
|
||||
const { response, isRetried } = await this.executeWithCaeRetry(processedConfig, resource);
|
||||
if (this.logger) {
|
||||
this.logRequest(config, processedConfig, response, startTime, {
|
||||
resource,
|
||||
resolvedEndpointHeaders,
|
||||
isRetried,
|
||||
extractTelemetryProperties,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
catch (error) {
|
||||
if (this.logger) {
|
||||
const status = error instanceof HttpError ? error.status : undefined;
|
||||
const responseHeaders = error instanceof HttpError ? error.responseHeaders : undefined;
|
||||
this.logRequest(config, processedConfig, { status, headers: responseHeaders }, startTime, {
|
||||
resource,
|
||||
resolvedEndpointHeaders,
|
||||
isRetried: false,
|
||||
extractTelemetryProperties,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** Execute fetch with CAE retry logic. Returns the response and whether a retry occurred. */
|
||||
async executeWithCaeRetry(processedConfig, resource) {
|
||||
try {
|
||||
const response = await this.executeFetch(processedConfig);
|
||||
return { response, isRetried: false };
|
||||
}
|
||||
catch (error) {
|
||||
// CAE retry: on 401 with claims challenge, re-acquire token and retry once
|
||||
if (this.authProvider && resource && error instanceof HttpError && error.status === 401) {
|
||||
const claims = parseCaeChallenge(error.responseHeaders?.['www-authenticate']);
|
||||
if (claims) {
|
||||
const freshToken = await this.authProvider.getToken({
|
||||
resource,
|
||||
claims,
|
||||
bypassCache: true,
|
||||
});
|
||||
const retryConfig = {
|
||||
...processedConfig,
|
||||
headers: {
|
||||
...processedConfig.headers,
|
||||
Authorization: `Bearer ${freshToken.accessToken}`,
|
||||
},
|
||||
};
|
||||
const response = await this.executeFetch(retryConfig);
|
||||
return { response, isRetried: true };
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Execute the raw fetch, parse the response, throw HttpError on !ok,
|
||||
* and apply response interceptors.
|
||||
*/
|
||||
async executeFetch(processedConfig) {
|
||||
const method = processedConfig.method ?? 'GET';
|
||||
const headers = {
|
||||
...this.defaultHeaders,
|
||||
...processedConfig.headers,
|
||||
};
|
||||
// Auto-set Content-Type for object bodies
|
||||
if (processedConfig.body !== undefined &&
|
||||
typeof processedConfig.body === 'object' &&
|
||||
!headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
const timeoutMs = processedConfig.timeoutMs ?? this.defaultTimeoutMs;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
// Compose signals if caller provided one
|
||||
const signal = processedConfig.signal
|
||||
? this.composeAbortSignals(processedConfig.signal, controller.signal)
|
||||
: controller.signal;
|
||||
try {
|
||||
const response = await this.fetchFn(processedConfig.url, {
|
||||
method,
|
||||
headers,
|
||||
body: processedConfig.body !== undefined
|
||||
? typeof processedConfig.body === 'string'
|
||||
? processedConfig.body
|
||||
: JSON.stringify(processedConfig.body)
|
||||
: undefined,
|
||||
signal,
|
||||
});
|
||||
const responseHeaders = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
responseHeaders[key] = value;
|
||||
});
|
||||
let data;
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
data = (await response.json());
|
||||
}
|
||||
else {
|
||||
data = (await response.text());
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new HttpError(`HTTP ${response.status}: ${response.statusText}`, processedConfig, response.status, data, responseHeaders);
|
||||
}
|
||||
let result = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data,
|
||||
headers: responseHeaders,
|
||||
};
|
||||
// Apply response interceptors
|
||||
for (const interceptor of this.responseInterceptors) {
|
||||
result = await interceptor(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new HttpError('Request timed out', processedConfig);
|
||||
}
|
||||
throw new HttpError(error instanceof Error ? error.message : 'Unknown request error', processedConfig);
|
||||
}
|
||||
finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resolve a URL to its final form and determine the auth resource.
|
||||
*
|
||||
* 1. Absolute URL — used as-is; resource resolved by matching registered
|
||||
* endpoint baseUrls (or undefined if no match).
|
||||
* 2. Service name prefix — first path segment matches a registered endpoint.
|
||||
* 3. Unresolved — passed through as-is; no resource.
|
||||
*/
|
||||
resolveUrl(url) {
|
||||
// Absolute URL
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
// Check if URL starts with any registered endpoint's baseUrl
|
||||
for (const endpoint of this.endpoints.values()) {
|
||||
if (url.startsWith(endpoint.baseUrl)) {
|
||||
return {
|
||||
resolvedUrl: url,
|
||||
resource: endpoint.resource,
|
||||
endpointHeaders: endpoint.headers,
|
||||
extractTelemetryProperties: endpoint.extractTelemetryProperties,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { resolvedUrl: url };
|
||||
}
|
||||
// Service name prefix: check if the first path segment matches a registered endpoint
|
||||
const slashIndex = url.indexOf('/');
|
||||
const firstSegment = slashIndex === -1 ? url : url.substring(0, slashIndex);
|
||||
const endpoint = this.endpoints.get(firstSegment);
|
||||
if (endpoint) {
|
||||
const remainingPath = slashIndex === -1 ? '' : url.substring(slashIndex);
|
||||
const base = endpoint.baseUrl.replace(/\/$/, '');
|
||||
const resolvedUrl = remainingPath ? `${base}/${remainingPath.replace(/^\//, '')}` : base;
|
||||
return {
|
||||
resolvedUrl,
|
||||
resource: endpoint.resource,
|
||||
endpointHeaders: endpoint.headers,
|
||||
extractTelemetryProperties: endpoint.extractTelemetryProperties,
|
||||
};
|
||||
}
|
||||
return { resolvedUrl: url };
|
||||
}
|
||||
composeAbortSignals(userSignal, timeoutSignal) {
|
||||
const controller = new AbortController();
|
||||
const onAbort = () => controller.abort();
|
||||
userSignal.addEventListener('abort', onAbort, { once: true });
|
||||
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
||||
return controller.signal;
|
||||
}
|
||||
/** Assemble and emit the HTTP telemetry event. */
|
||||
logRequest(originalConfig, processedConfig, outcome, startTime, context) {
|
||||
const duration = performance.now() - startTime;
|
||||
const status = outcome.status ?? 0;
|
||||
const succeeded = status >= 200 && status < 300;
|
||||
const responseHeaders = outcome.headers;
|
||||
// Layer 1: Endpoint-extracted properties
|
||||
const endpointProps = context.extractTelemetryProperties
|
||||
? context.extractTelemetryProperties(originalConfig)
|
||||
: {};
|
||||
// Layer 2: Per-request telemetry properties
|
||||
const requestProps = originalConfig.telemetryProperties ?? {};
|
||||
// Layer 3: Computed properties (wins over layers 1 & 2)
|
||||
const computedProps = {
|
||||
url: processedConfig.url,
|
||||
httpMethod: processedConfig.method ?? 'GET',
|
||||
httpStatus: status,
|
||||
duration,
|
||||
succeeded,
|
||||
isRetried: context.isRetried,
|
||||
};
|
||||
if (context.resource) {
|
||||
computedProps.resourceUrl = context.resource;
|
||||
}
|
||||
// Log all resolved endpoint headers as telemetry properties
|
||||
if (context.resolvedEndpointHeaders) {
|
||||
for (const [name, value] of Object.entries(context.resolvedEndpointHeaders)) {
|
||||
computedProps[`requestHeader.${name}`] = value;
|
||||
}
|
||||
}
|
||||
const authHeader = processedConfig.headers?.['Authorization'];
|
||||
if (authHeader) {
|
||||
computedProps.requestAuthorizationHeaderLength = authHeader.length;
|
||||
}
|
||||
if (responseHeaders) {
|
||||
const correlationRequestId = responseHeaders['x-ms-correlation-request-id'];
|
||||
if (correlationRequestId) {
|
||||
computedProps.correlationRequestId = correlationRequestId;
|
||||
}
|
||||
const serverRequestId = responseHeaders['x-ms-request-id'];
|
||||
if (serverRequestId) {
|
||||
computedProps.serverRequestId = serverRequestId;
|
||||
}
|
||||
const contentType = responseHeaders['content-type'];
|
||||
if (contentType) {
|
||||
computedProps.responseContentTypeHeader = contentType;
|
||||
}
|
||||
const contentLength = responseHeaders['content-length'];
|
||||
if (contentLength) {
|
||||
computedProps.contentLength = contentLength;
|
||||
}
|
||||
const serverLocation = responseHeaders['x-ms-server-location'];
|
||||
if (serverLocation) {
|
||||
computedProps.serverLocation = serverLocation;
|
||||
}
|
||||
}
|
||||
// Merge: endpoint → request → computed (later wins)
|
||||
const properties = { ...endpointProps, ...requestProps, ...computedProps };
|
||||
this.logger.trackActivityEvent(originalConfig.operationName ?? 'unknown', properties, 'HttpClient.SendHttpAsync');
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=HttpClient.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
import { SdkError } from '../errors/SdkError.js';
|
||||
export interface HttpRequestConfig {
|
||||
/** Request URL (absolute, service-name prefixed, or relative). */
|
||||
url: string;
|
||||
/** HTTP method. Defaults to 'GET'. */
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
/** Request headers. */
|
||||
headers?: Record<string, string>;
|
||||
/** Request body (will be JSON-serialized if an object). */
|
||||
body?: unknown;
|
||||
/** Request timeout in milliseconds. */
|
||||
timeoutMs?: number;
|
||||
/** AbortSignal for request cancellation. */
|
||||
signal?: AbortSignal;
|
||||
/** API operation name for telemetry (e.g. 'Connections.GetConnections'). Defaults to 'unknown'. */
|
||||
operationName?: string;
|
||||
/** Additional telemetry properties merged into the HTTP telemetry event. */
|
||||
telemetryProperties?: Record<string, unknown>;
|
||||
}
|
||||
/** Typed HTTP response. */
|
||||
export interface HttpResponse<T = unknown> {
|
||||
/** Parsed response body. */
|
||||
data: T;
|
||||
/** Response headers. */
|
||||
headers: Record<string, string>;
|
||||
/** Response status code. */
|
||||
status: number;
|
||||
/** Response status text. */
|
||||
statusText: string;
|
||||
}
|
||||
/** Error thrown when an HTTP request fails. */
|
||||
export declare class HttpError extends SdkError {
|
||||
/** HTTP status code, or undefined if the request did not complete. */
|
||||
readonly status?: number;
|
||||
/** The original request config. */
|
||||
readonly config: HttpRequestConfig;
|
||||
/** Parsed response body, if available. */
|
||||
readonly responseData?: unknown;
|
||||
/** Parsed response headers, if available. */
|
||||
readonly responseHeaders?: Record<string, string>;
|
||||
constructor(message: string, config: HttpRequestConfig, status?: number, responseData?: unknown, responseHeaders?: Record<string, string>);
|
||||
}
|
||||
/** Interceptor that can modify a request before it is sent. */
|
||||
export type RequestInterceptor = (config: HttpRequestConfig) => HttpRequestConfig | Promise<HttpRequestConfig>;
|
||||
/** Interceptor that can modify a response before it is returned. */
|
||||
export type ResponseInterceptor = <T>(response: HttpResponse<T>) => HttpResponse<T> | Promise<HttpResponse<T>>;
|
||||
//# sourceMappingURL=HttpTypes.d.ts.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"HttpTypes.d.ts","sourceRoot":"","sources":["../../src/http/HttpTypes.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACrD,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mGAAmG;IACnG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C;AAED,2BAA2B;AAC3B,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,4BAA4B;IAC5B,IAAI,EAAE,CAAC,CAAC;IACR,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,+CAA+C;AAC/C,qBAAa,SAAU,SAAQ,QAAQ;IACrC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,mCAAmC;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,6CAA6C;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAGhD,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,OAAO,EACtB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CAS3C;AAED,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG,CAC/B,MAAM,EAAE,iBAAiB,KACtB,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEpD,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,EAClC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,KACtB,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
import { SdkError } from '../errors/SdkError.js';
|
||||
/** Error thrown when an HTTP request fails. */
|
||||
export class HttpError extends SdkError {
|
||||
/** HTTP status code, or undefined if the request did not complete. */
|
||||
status;
|
||||
/** The original request config. */
|
||||
config;
|
||||
/** Parsed response body, if available. */
|
||||
responseData;
|
||||
/** Parsed response headers, if available. */
|
||||
responseHeaders;
|
||||
constructor(message, config, status, responseData, responseHeaders) {
|
||||
super('HTTP_REQUEST_FAILED', message);
|
||||
this.name = 'HttpError';
|
||||
this.config = config;
|
||||
this.status = status;
|
||||
this.responseData = responseData;
|
||||
this.responseHeaders = responseHeaders;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=HttpTypes.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"HttpTypes.js","sourceRoot":"","sources":["../../src/http/HttpTypes.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAiC9C,+CAA+C;AAC/C,MAAM,OAAO,SAAU,SAAQ,QAAQ;IACrC,sEAAsE;IAC7D,MAAM,CAAU;IACzB,mCAAmC;IAC1B,MAAM,CAAoB;IACnC,0CAA0C;IACjC,YAAY,CAAW;IAChC,6CAA6C;IACpC,eAAe,CAA0B;IAElD,YACE,OAAe,EACf,MAAyB,EACzB,MAAe,EACf,YAAsB,EACtB,eAAwC;QAExC,KAAK,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IACzC,CAAC;CACF"}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
export { parseCaeChallenge } from './CaeUtils.js';
|
||||
export { HttpClient, HttpClientOptions } from './HttpClient.js';
|
||||
export { HttpError, HttpRequestConfig, HttpResponse, RequestInterceptor, ResponseInterceptor, } from './HttpTypes.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,aAAa,CAAC"}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
export { parseCaeChallenge } from './CaeUtils.js';
|
||||
export { HttpClient } from './HttpClient.js';
|
||||
export { HttpError, } from './HttpTypes.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAqB,MAAM,cAAc,CAAC;AAC7D,OAAO,EACL,SAAS,GAKV,MAAM,aAAa,CAAC"}
|
||||
Reference in New Issue
Block a user