@azure/service-bus
Version:
Azure Service Bus SDK for JavaScript
378 lines • 15.2 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { RestError, } from "@azure/core-rest-pipeline";
import { parseXML, stringifyXML } from "@azure/core-xml";
import * as Constants from "./constants";
import { administrationLogger as logger } from "../log";
import { Buffer } from "buffer";
import { parseURL } from "./parseUrl";
import { isJSONLikeObject } from "./utils";
import { isDefined } from "@azure/core-util";
/**
applies options to the pipeline request.
*/
function applyRequestOptions(request, options) {
if (options.headers) {
const headers = options.headers;
for (const headerName of Object.keys(headers)) {
request.headers.set(headerName, headers[headerName]);
}
}
request.onDownloadProgress = options.onDownloadProgress;
request.onUploadProgress = options.onUploadProgress;
request.abortSignal = options.abortSignal;
request.timeout = options.timeout;
if (options.tracingOptions) {
request.tracingOptions = options.tracingOptions;
}
}
/**
* @internal
* Utility to execute Atom XML operations as HTTP requests
*/
export async function executeAtomXmlOperation(serviceBusAtomManagementClient, request, serializer, operationOptions, requestObject) {
if (requestObject) {
request.body = stringifyXML(serializer.serialize(requestObject), { rootName: "entry" });
if (request.method === "PUT") {
request.headers.set("content-length", Buffer.byteLength(request.body));
}
}
logger.verbose(`Executing ATOM based HTTP request: ${request.body}`);
const reqPrepareOptions = {
headers: operationOptions.requestOptions?.customHeaders,
onUploadProgress: operationOptions.requestOptions?.onUploadProgress,
onDownloadProgress: operationOptions.requestOptions?.onDownloadProgress,
abortSignal: operationOptions.abortSignal,
tracingOptions: operationOptions.tracingOptions,
disableJsonStringifyOnBody: true,
timeout: operationOptions.requestOptions?.timeout || 0,
};
applyRequestOptions(request, reqPrepareOptions);
const response = await serviceBusAtomManagementClient.sendRequest(request);
logger.verbose(`Received ATOM based HTTP response: ${response.bodyAsText}`);
try {
if (response.bodyAsText) {
response.parsedBody = await parseXML(response.bodyAsText, {
includeRoot: true,
});
}
}
catch (err) {
const error = new RestError(`Error occurred while parsing the response body - expected the service to return valid xml content.`, {
code: RestError.PARSE_ERROR,
statusCode: response.status,
request: response.request,
response,
});
logger.logError(err, "Error parsing response body from Service");
throw error;
}
return serializer.deserialize(response);
}
/**
* @internal
* The key-value pairs having undefined/null as the values would lead to the empty tags in the serialized XML request.
* Empty tags in the request body is problematic because of the following reasons.
* - ATOM based management operations throw a "Bad Request" error if empty tags are included in the XML request body at top level.
* - At the inner levels, Service assigns the empty strings as values to the empty tags instead of throwing an error.
*
* This method recursively removes the key-value pairs with undefined/null as the values from the request object that is to be serialized.
*
*/
export function sanitizeSerializableObject(resource) {
Object.keys(resource).forEach(function (property) {
if (!isDefined(resource[property])) {
delete resource[property];
}
else if (isJSONLikeObject(resource[property])) {
sanitizeSerializableObject(resource[property]);
}
});
}
/**
* @internal
* Serializes input information to construct the Atom XML request
* @param resourceName - Name of the resource to be serialized like `QueueDescription`
* @param resource - The entity details
* @param allowedProperties - The set of properties that are allowed by the service for the
* associated operation(s);
*/
export function serializeToAtomXmlRequest(resourceName, resource) {
const content = {};
content[resourceName] = Object.assign({}, resource);
sanitizeSerializableObject(content[resourceName]);
content[resourceName][Constants.XML_METADATA_MARKER] = {
xmlns: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect",
"xmlns:i": "http://www.w3.org/2001/XMLSchema-instance",
};
content[Constants.XML_METADATA_MARKER] = { type: "application/xml" };
const requestDetails = {
updated: new Date().toISOString(),
content: content,
};
requestDetails[Constants.XML_METADATA_MARKER] = {
xmlns: "http://www.w3.org/2005/Atom",
};
return requestDetails;
}
/**
* @internal
* Transforms response to contain the parsed data.
* @param nameProperties - The set of 'name' properties to be constructed on the
* resultant object e.g., QueueName, TopicName, etc.
*/
export async function deserializeAtomXmlResponse(nameProperties, response) {
// If received data is a non-valid HTTP response, the body is expected to contain error information
if (response.status < 200 || response.status >= 300) {
throw buildError(response);
}
parseAtomResult(response, nameProperties);
return response;
}
/**
* @internal
* Utility to deserialize the given JSON content in response body based on
* if it's a single `entry` or `feed` and updates the `response.parsedBody` to hold the evaluated output.
* @param response - Response containing the JSON value in `response.parsedBody`
* @param nameProperties - The set of 'name' properties to be constructed on the
* resultant object e.g., QueueName, TopicName, etc.
* */
function parseAtomResult(response, nameProperties) {
const atomResponseInJson = response.parsedBody;
let result;
if (!atomResponseInJson) {
response.parsedBody = undefined;
return;
}
if (atomResponseInJson.feed) {
result = parseFeedResult(atomResponseInJson.feed);
}
else if (atomResponseInJson.entry) {
result = parseEntryResult(atomResponseInJson.entry);
}
if (result) {
if (Array.isArray(result)) {
result.forEach((entry) => {
setName(entry, nameProperties);
});
}
else {
setName(result, nameProperties);
}
response.parsedBody = result;
return;
}
logger.warning("Failure in parsing response body from service. Expected response to be in Atom XML format and have either feed or entry components, but received - %0", atomResponseInJson);
throw new RestError("Error occurred while parsing the response body - expected the service to return atom xml content with either feed or entry elements.", {
code: RestError.PARSE_ERROR,
statusCode: response.status,
request: response.request,
response,
});
}
/**
* @internal
* Utility to help parse given `entry` result
*/
function parseEntryResult(entry) {
let result;
if (typeof entry !== "object" ||
entry == null ||
typeof entry.content !== "object" ||
entry.content == null) {
return undefined;
}
const contentElementNames = Object.keys(entry.content).filter(function (key) {
return key !== Constants.XML_METADATA_MARKER;
});
if (contentElementNames && contentElementNames[0]) {
const contentRootElementName = contentElementNames[0];
delete entry.content[contentRootElementName][Constants.XML_METADATA_MARKER];
result = entry.content[contentRootElementName];
if (result) {
if (entry[Constants.XML_METADATA_MARKER]) {
result[Constants.ATOM_METADATA_MARKER] = entry[Constants.XML_METADATA_MARKER];
}
else {
result[Constants.ATOM_METADATA_MARKER] = {};
}
result[Constants.ATOM_METADATA_MARKER]["ContentRootElement"] = contentRootElementName;
Object.keys(entry).forEach((property) => {
if (property !== "content" && property !== Constants.XML_METADATA_MARKER) {
result[Constants.ATOM_METADATA_MARKER][property] = entry[property];
}
});
return result;
}
}
return undefined;
}
/**
* @internal
* Utility to help parse link info from the given `feed` result
*/
function parseLinkInfo(feedLink, relationship) {
if (!feedLink || !Array.isArray(feedLink)) {
return undefined;
}
for (const linkInfo of feedLink) {
if (linkInfo[Constants.XML_METADATA_MARKER].rel === relationship) {
return linkInfo[Constants.XML_METADATA_MARKER].href;
}
}
return undefined;
}
/**
* @internal
* Utility to help parse given `feed` result
*/
function parseFeedResult(feed) {
const result = [];
if (typeof feed === "object" && feed != null && feed.entry) {
if (Array.isArray(feed.entry)) {
feed.entry.forEach((entry) => {
const parsedEntryResult = parseEntryResult(entry);
if (parsedEntryResult) {
result.push(parsedEntryResult);
}
});
}
else {
const parsedEntryResult = parseEntryResult(feed.entry);
if (parsedEntryResult) {
result.push(parsedEntryResult);
}
}
result.nextLink = parseLinkInfo(feed.link, "next");
}
return result;
}
/**
* @internal
*/
function isKnownResponseCode(statusCode) {
return !!Constants.HttpResponseCodes[statusCode];
}
/**
* @internal
* Extracts the applicable entity name(s) from the URL based on the known structure
* and instantiates the corresponding name properties to the deserialized response
*
* The pattern matching checks to extract entity names are based on following
* constraints dictated by the service
* - '/' is allowed in Queue and Topic names
* - '/' is not allowed in Namespace, Subscription and Rule names
* - Valid pathname URL structures used in the ATOM based management API are
* - `<namespace-component>/<topic-name>/Subscriptions/<subscription-name>/Rules/<rule-name>`
* - `<namespace-component>/<topic-name>/Subscriptions/<subscription-name>`
* - `<namespace-component>/<any-entity-name>`
*
*/
function setName(entry, nameProperties) {
if (entry[Constants.ATOM_METADATA_MARKER]) {
let rawUrl = entry[Constants.ATOM_METADATA_MARKER].id;
// The parsedUrl gets constructed differently for browser vs Node.
// It is specifically behaves different for some of the Atom based management API where
// the received URL in "id" element is of type "sb:// ... " and not a standard HTTP one
// Hence, normalizing the URL for parsing to work as expected in browser
if (rawUrl.startsWith("sb://")) {
rawUrl = "https://" + rawUrl.substring(5);
}
const parsedUrl = parseURL(rawUrl);
const pathname = parsedUrl.pathname;
const firstIndexOfDelimiter = pathname.indexOf("/");
if (pathname.match("(.*)/(.*)/Subscriptions/(.*)/Rules/(.*)")) {
const lastIndexOfSubscriptionsDelimiter = pathname.lastIndexOf("/Subscriptions/");
const firstIndexOfRulesDelimiter = pathname.indexOf("/Rules/");
entry[nameProperties[0]] = pathname.substring(firstIndexOfDelimiter + 1, lastIndexOfSubscriptionsDelimiter);
entry[nameProperties[1]] = pathname.substring(lastIndexOfSubscriptionsDelimiter + 15, firstIndexOfRulesDelimiter);
entry[nameProperties[2]] = pathname.substring(firstIndexOfRulesDelimiter + 7);
}
else if (pathname.match("(.*)/(.*)/Subscriptions/(.*)")) {
const lastIndexOfSubscriptionsDelimiter = pathname.lastIndexOf("/Subscriptions/");
entry[nameProperties[0]] = pathname.substring(firstIndexOfDelimiter + 1, lastIndexOfSubscriptionsDelimiter);
entry[nameProperties[1]] = pathname.substring(lastIndexOfSubscriptionsDelimiter + 15);
}
else if (pathname.match("(.*)/(.*)")) {
entry[nameProperties[0]] = pathname.substring(firstIndexOfDelimiter + 1);
}
}
}
/**
* @internal
* Utility to help construct the normalized `RestError` object based on given error
* information and other data present in the received `response` object.
*/
export function buildError(response) {
if (!isKnownResponseCode(response.status)) {
throw new RestError(`Service returned an error response with an unrecognized HTTP status code - ${response.status}`, {
code: "ServiceError",
statusCode: response.status,
request: response.request,
response,
});
}
const errorBody = response.parsedBody;
let errorMessage;
if (typeof errorBody === "string") {
errorMessage = errorBody;
}
else {
if (!isDefined(errorBody) ||
!isDefined(errorBody.Error) ||
!isDefined(errorBody.Error.Detail)) {
errorMessage =
"Detailed error message information not available. Look at the 'code' property on error for more information.";
}
else {
errorMessage = errorBody.Error.Detail;
}
}
const errorCode = getErrorCode(response, errorMessage);
const error = new RestError(errorMessage, {
code: errorCode,
statusCode: response.status,
request: response.request,
response,
});
return error;
}
/**
* @internal
* Helper utility to construct user friendly error codes based on based on given error
* information and other data present in the received `response` object.
*/
function getErrorCode(response, errorMessage) {
if (response.status === 401) {
return "UnauthorizedRequestError";
}
if (response.status === 404) {
return "MessageEntityNotFoundError";
}
if (response.status === 409) {
if (response.request.method === "DELETE") {
return "ServiceError";
}
if (response.request.method === "PUT" && response.request.headers.get("If-Match") === "*") {
return "ServiceError";
}
if (errorMessage && errorMessage.toLowerCase().includes("subcode=40901")) {
return "ServiceError";
}
return "MessageEntityAlreadyExistsError";
}
if (response.status === 403) {
if (errorMessage && errorMessage.toLowerCase().includes("subcode=40301")) {
return "InvalidOperationError";
}
return "QuotaExceededError";
}
if (response.status === 400) {
return "ServiceError";
}
if (response.status === 503) {
return "ServerBusyError";
}
return Constants.HttpResponseCodes[response.status];
}
//# sourceMappingURL=atomXmlHelper.js.map