@microsoft/kiota-http-fetchlibrary
Version:
Kiota request adapter implementation with fetch
616 lines • 34.4 kB
JavaScript
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { InMemoryBackingStoreFactory, DefaultApiError, enableBackingStoreForParseNodeFactory, enableBackingStoreForSerializationWriterFactory, ParseNodeFactoryRegistry, ResponseHandlerOptionKey, SerializationWriterFactoryRegistry } from "@microsoft/kiota-abstractions";
import { SpanStatusCode, trace } from "@opentelemetry/api";
import { HttpClient } from "./httpClient.js";
import { ObservabilityOptionsImpl } from "./observabilityOptions.js";
/**
* Request adapter implementation for the fetch API.
*/
export class FetchRequestAdapter {
getSerializationWriterFactory() {
return this.serializationWriterFactory;
}
getParseNodeFactory() {
return this.parseNodeFactory;
}
getBackingStoreFactory() {
return this.backingStoreFactory;
}
/**
* Instantiates a new request adapter.
* @param authenticationProvider the authentication provider to use.
* @param parseNodeFactory the parse node factory to deserialize responses.
* @param serializationWriterFactory the serialization writer factory to use to serialize request bodies.
* @param httpClient the http client to use to execute requests.
* @param observabilityOptions the observability options to use.
* @param backingStoreFactory the backing store factory to use.
*/
constructor(authenticationProvider, parseNodeFactory = new ParseNodeFactoryRegistry(), serializationWriterFactory = new SerializationWriterFactoryRegistry(), httpClient = new HttpClient(), observabilityOptions = new ObservabilityOptionsImpl(), backingStoreFactory = new InMemoryBackingStoreFactory()) {
this.authenticationProvider = authenticationProvider;
this.parseNodeFactory = parseNodeFactory;
this.serializationWriterFactory = serializationWriterFactory;
this.httpClient = httpClient;
this.backingStoreFactory = backingStoreFactory;
/** The base url for every request. */
this.baseUrl = "";
this.getResponseContentType = (response) => {
var _a;
const header = (_a = response.headers.get("content-type")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
if (!header)
return undefined;
const segments = header.split(";");
if (segments.length === 0)
return undefined;
else
return segments[0];
};
this.getResponseHandler = (response) => {
const options = response.getRequestOptions();
const responseHandlerOption = options[ResponseHandlerOptionKey];
return responseHandlerOption === null || responseHandlerOption === void 0 ? void 0 : responseHandlerOption.responseHandler;
};
this.sendCollectionOfPrimitive = (requestInfo, responseType, errorMappings) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendCollectionOfPrimitive", async (span) => {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
}
else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response))
return undefined;
switch (responseType) {
case "string":
case "number":
case "boolean":
case "Date":
// eslint-disable-next-line no-case-declarations
const rootNode = await this.getRootParseNode(response);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan(`getCollectionOf${responseType}Value`, (deserializeSpan) => {
try {
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, responseType);
if (responseType === "string") {
return rootNode.getCollectionOfPrimitiveValues();
}
else if (responseType === "number") {
return rootNode.getCollectionOfPrimitiveValues();
}
else if (responseType === "boolean") {
return rootNode.getCollectionOfPrimitiveValues();
}
else if (responseType === "Date") {
return rootNode.getCollectionOfPrimitiveValues();
}
else if (responseType === "Duration") {
return rootNode.getCollectionOfPrimitiveValues();
}
else if (responseType === "DateOnly") {
return rootNode.getCollectionOfPrimitiveValues();
}
else if (responseType === "TimeOnly") {
return rootNode.getCollectionOfPrimitiveValues();
}
else {
throw new Error("unexpected type to deserialize");
}
}
finally {
deserializeSpan.end();
}
});
}
}
finally {
await this.purgeResponseBody(response);
}
}
});
};
this.sendCollection = (requestInfo, deserialization, errorMappings) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendCollection", async (span) => {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
}
else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response))
return undefined;
const rootNode = await this.getRootParseNode(response);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getCollectionOfObjectValues", (deserializeSpan) => {
try {
const result = rootNode.getCollectionOfObjectValues(deserialization);
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, "object[]");
return result;
}
finally {
deserializeSpan.end();
}
});
}
finally {
await this.purgeResponseBody(response);
}
}
});
};
this.startTracingSpan = (requestInfo, methodName, callback) => {
var _a;
const urlTemplate = decodeURIComponent((_a = requestInfo.urlTemplate) !== null && _a !== void 0 ? _a : "");
const telemetryPathValue = urlTemplate.replace(/\{\?[^}]+\}/gi, "");
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan(`${methodName} - ${telemetryPathValue}`, async (span) => {
try {
span.setAttribute("url.uri_template", urlTemplate);
return await callback(span);
}
finally {
span.end();
}
});
};
this.send = (requestInfo, deserializer, errorMappings) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "send", async (span) => {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
}
else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response))
return undefined;
const rootNode = await this.getRootParseNode(response);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getObjectValue", (deserializeSpan) => {
try {
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, "object");
const result = rootNode.getObjectValue(deserializer);
return result;
}
finally {
deserializeSpan.end();
}
});
}
finally {
await this.purgeResponseBody(response);
}
}
});
};
this.sendPrimitive = (requestInfo, responseType, errorMappings) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendPrimitive", async (span) => {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
}
else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response))
return undefined;
switch (responseType) {
case "ArrayBuffer":
if (!response.body) {
return undefined;
}
return (await response.arrayBuffer());
case "string":
case "number":
case "boolean":
case "Date":
// eslint-disable-next-line no-case-declarations
const rootNode = await this.getRootParseNode(response);
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, responseType);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan(`get${responseType}Value`, (deserializeSpan) => {
try {
if (responseType === "string") {
return rootNode.getStringValue();
}
else if (responseType === "number") {
return rootNode.getNumberValue();
}
else if (responseType === "boolean") {
return rootNode.getBooleanValue();
}
else if (responseType === "Date") {
return rootNode.getDateValue();
}
else if (responseType === "Duration") {
return rootNode.getDurationValue();
}
else if (responseType === "DateOnly") {
return rootNode.getDateOnlyValue();
}
else if (responseType === "TimeOnly") {
return rootNode.getTimeOnlyValue();
}
else {
throw new Error("unexpected type to deserialize");
}
}
finally {
deserializeSpan.end();
}
});
}
}
finally {
await this.purgeResponseBody(response);
}
}
});
};
this.sendNoResponseContent = (requestInfo, errorMappings) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendNoResponseContent", async (span) => {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
}
try {
await this.throwIfFailedResponse(response, errorMappings, span);
}
finally {
await this.purgeResponseBody(response);
}
});
};
this.sendEnum = (requestInfo, enumObject, errorMappings) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendEnum", async (span) => {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
}
else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response))
return undefined;
const rootNode = await this.getRootParseNode(response);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getEnumValue", (deserializeSpan) => {
try {
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, "enum");
const result = rootNode.getEnumValue(enumObject);
return result;
}
finally {
deserializeSpan.end();
}
});
}
finally {
await this.purgeResponseBody(response);
}
}
});
};
this.sendCollectionOfEnum = (requestInfo, enumObject, errorMappings) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendCollectionOfEnum", async (span) => {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
}
else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response))
return undefined;
const rootNode = await this.getRootParseNode(response);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getCollectionOfEnumValues", (deserializeSpan) => {
try {
const result = rootNode.getCollectionOfEnumValues(enumObject);
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, "enum[]");
return result;
}
finally {
deserializeSpan.end();
}
});
}
finally {
await this.purgeResponseBody(response);
}
}
});
};
this.enableBackingStore = (backingStoreFactory) => {
if (this.parseNodeFactory instanceof ParseNodeFactoryRegistry) {
this.parseNodeFactory = enableBackingStoreForParseNodeFactory(this.parseNodeFactory, this.parseNodeFactory);
}
else {
throw new Error("parseNodeFactory is not a ParseNodeFactoryRegistry");
}
if (this.serializationWriterFactory instanceof SerializationWriterFactoryRegistry && this.parseNodeFactory instanceof ParseNodeFactoryRegistry) {
this.serializationWriterFactory = enableBackingStoreForSerializationWriterFactory(this.serializationWriterFactory, this.parseNodeFactory, this.serializationWriterFactory);
}
else {
throw new Error("serializationWriterFactory is not a SerializationWriterFactoryRegistry or parseNodeFactory is not a ParseNodeFactoryRegistry");
}
if (!this.serializationWriterFactory || !this.parseNodeFactory)
throw new Error("unable to enable backing store");
if (backingStoreFactory) {
this.backingStoreFactory = backingStoreFactory;
}
};
this.getRootParseNode = (response) => {
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getRootParseNode", async (span) => {
try {
const payload = await response.arrayBuffer();
const responseContentType = this.getResponseContentType(response);
if (!responseContentType)
throw new Error("no response content type found for deserialization");
return this.parseNodeFactory.getRootParseNode(responseContentType, payload);
}
finally {
span.end();
}
});
};
this.shouldReturnUndefined = (response) => {
return response.status === 204 || response.status === 304 || !response.body;
};
/**
* purges the response body if it hasn't been read to release the connection to the server
* @param response the response to purge
*/
this.purgeResponseBody = async (response) => {
if (!response.bodyUsed && response.body) {
await response.arrayBuffer();
}
};
this.throwIfFailedResponse = (response, errorMappings, spanForAttributes) => {
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("throwIfFailedResponse", async (span) => {
var _a, _b, _c;
try {
if (response.ok || (response.status >= 300 && response.status < 400 && !response.headers.has(FetchRequestAdapter.locationHeaderName)))
return;
spanForAttributes.setStatus({
code: SpanStatusCode.ERROR,
message: "received_error_response",
});
const statusCode = response.status;
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value.split(",");
});
const factory = errorMappings ? ((_c = (_b = (_a = errorMappings[statusCode]) !== null && _a !== void 0 ? _a : (statusCode >= 400 && statusCode < 500 ? errorMappings._4XX : undefined)) !== null && _b !== void 0 ? _b : (statusCode >= 500 && statusCode < 600 ? errorMappings._5XX : undefined)) !== null && _c !== void 0 ? _c : errorMappings.XXX) : undefined;
if (!factory) {
spanForAttributes.setAttribute(FetchRequestAdapter.errorMappingFoundAttributeName, false);
const error = new DefaultApiError("the server returned an unexpected status code and no error class is registered for this code " + statusCode);
error.responseStatusCode = statusCode;
error.responseHeaders = responseHeaders;
spanForAttributes.recordException(error);
throw error;
}
spanForAttributes.setAttribute(FetchRequestAdapter.errorMappingFoundAttributeName, true);
const rootNode = await this.getRootParseNode(response);
let deserializedError = trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getObjectValue", (deserializeSpan) => {
try {
return rootNode.getObjectValue(factory);
}
finally {
deserializeSpan.end();
}
});
spanForAttributes.setAttribute(FetchRequestAdapter.errorBodyFoundAttributeName, !!deserializedError);
if (!deserializedError)
deserializedError = new DefaultApiError("unexpected error type" + typeof deserializedError);
const errorObject = deserializedError;
errorObject.responseStatusCode = statusCode;
errorObject.responseHeaders = responseHeaders;
spanForAttributes.recordException(errorObject);
throw errorObject;
}
finally {
span.end();
}
});
};
this.getHttpResponseMessage = (requestInfo, spanForAttributes, claims) => {
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getHttpResponseMessage", async (span) => {
try {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
this.setBaseUrlForRequestInformation(requestInfo);
const additionalContext = {};
if (claims) {
additionalContext.claims = claims;
}
await this.authenticationProvider.authenticateRequest(requestInfo, additionalContext);
const request = await this.getRequestFromRequestInformation(requestInfo, spanForAttributes);
if (this.observabilityOptions) {
requestInfo.addRequestOptions([this.observabilityOptions]);
}
let response = await this.httpClient.executeFetch(requestInfo.URL, request, requestInfo.getRequestOptions());
response = await this.retryCAEResponseIfRequired(requestInfo, response, spanForAttributes, claims);
if (response) {
const responseContentLength = response.headers.get("Content-Length");
if (responseContentLength) {
spanForAttributes.setAttribute("http.response.body.size", parseInt(responseContentLength, 10));
}
const responseContentType = response.headers.get("Content-Type");
if (responseContentType) {
spanForAttributes.setAttribute("http.response.header.content-type", responseContentType);
}
spanForAttributes.setAttribute("http.response.status_code", response.status);
// getting the network.protocol.version (protocol version) is impossible with fetch API
}
return response;
}
finally {
span.end();
}
});
};
this.retryCAEResponseIfRequired = async (requestInfo, response, spanForAttributes, claims) => {
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("retryCAEResponseIfRequired", async (span) => {
try {
const responseClaims = this.getClaimsFromResponse(response, claims);
if (responseClaims) {
span.addEvent(FetchRequestAdapter.authenticateChallengedEventKey);
spanForAttributes.setAttribute("http.request.resend_count", 1);
await this.purgeResponseBody(response);
return await this.getHttpResponseMessage(requestInfo, spanForAttributes, responseClaims);
}
return response;
}
finally {
span.end();
}
});
};
this.getClaimsFromResponse = (response, claims) => {
if (response.status === 401 && !claims) {
// avoid infinite loop, we only retry once
// no need to check for the content since it's an array and it doesn't need to be rewound
const rawAuthenticateHeader = response.headers.get("WWW-Authenticate");
if (rawAuthenticateHeader && /^Bearer /gi.test(rawAuthenticateHeader)) {
const rawParameters = rawAuthenticateHeader.replace(/^Bearer /gi, "").split(",");
for (const rawParameter of rawParameters) {
const trimmedParameter = rawParameter.trim();
if (/claims="[^"]+"/gi.test(trimmedParameter)) {
return trimmedParameter.replace(/claims="([^"]+)"/gi, "$1");
}
}
}
}
return undefined;
};
this.setBaseUrlForRequestInformation = (requestInfo) => {
requestInfo.pathParameters.baseurl = this.baseUrl;
};
this.getRequestFromRequestInformation = (requestInfo, spanForAttributes) => {
// eslint-disable-next-line @typescript-eslint/require-await
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getRequestFromRequestInformation", async (span) => {
var _a, _b;
try {
const method = (_a = requestInfo.httpMethod) === null || _a === void 0 ? void 0 : _a.toString();
const uri = requestInfo.URL;
spanForAttributes.setAttribute("http.request.method", method !== null && method !== void 0 ? method : "");
const uriContainsScheme = uri.includes("://");
const schemeSplatUri = uri.split("://");
if (uriContainsScheme) {
spanForAttributes.setAttribute("server.address", schemeSplatUri[0]);
}
const uriWithoutScheme = uriContainsScheme ? schemeSplatUri[1] : uri;
spanForAttributes.setAttribute("url.scheme", uriWithoutScheme.split("/")[0]);
if (this.observabilityOptions.includeEUIIAttributes) {
spanForAttributes.setAttribute("url.full", decodeURIComponent(uri));
}
const requestContentLength = requestInfo.headers.tryGetValue("Content-Length");
if (requestContentLength) {
spanForAttributes.setAttribute("http.response.body.size", parseInt(requestContentLength[0], 10));
}
const requestContentType = requestInfo.headers.tryGetValue("Content-Type");
if (requestContentType) {
spanForAttributes.setAttribute("http.request.header.content-type", requestContentType);
}
const headers = {};
(_b = requestInfo.headers) === null || _b === void 0 ? void 0 : _b.forEach((_, key) => {
headers[key.toString().toLocaleLowerCase()] = this.foldHeaderValue(requestInfo.headers.tryGetValue(key));
});
const request = {
method,
headers,
body: requestInfo.content,
};
return request;
}
finally {
span.end();
}
});
};
this.foldHeaderValue = (value) => {
if (!value || value.length < 1) {
return "";
}
else if (value.length === 1) {
return value[0];
}
else {
return value.reduce((acc, val) => acc + val, ",");
}
};
/**
* @inheritdoc
*/
this.convertToNativeRequest = async (requestInfo) => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
await this.authenticationProvider.authenticateRequest(requestInfo, undefined);
return this.startTracingSpan(requestInfo, "convertToNativeRequest", async (span) => {
const request = await this.getRequestFromRequestInformation(requestInfo, span);
return request;
});
};
if (!authenticationProvider) {
throw new Error("authentication provider cannot be null");
}
if (!parseNodeFactory) {
throw new Error("parse node factory cannot be null");
}
if (!serializationWriterFactory) {
throw new Error("serialization writer factory cannot be null");
}
if (!httpClient) {
throw new Error("http client cannot be null");
}
if (!observabilityOptions) {
throw new Error("observability options cannot be null");
}
else {
this.observabilityOptions = new ObservabilityOptionsImpl(observabilityOptions);
}
}
}
FetchRequestAdapter.responseTypeAttributeKey = "com.microsoft.kiota.response.type";
FetchRequestAdapter.eventResponseHandlerInvokedKey = "com.microsoft.kiota.response_handler_invoked";
FetchRequestAdapter.errorMappingFoundAttributeName = "com.microsoft.kiota.error.mapping_found";
FetchRequestAdapter.errorBodyFoundAttributeName = "com.microsoft.kiota.error.body_found";
FetchRequestAdapter.locationHeaderName = "Location";
FetchRequestAdapter.authenticateChallengedEventKey = "com.microsoft.kiota.authenticate_challenge_received";
//# sourceMappingURL=fetchRequestAdapter.js.map