/** * ------------------------------------------------------------------------------------------- * 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