UNPKG

7.77 kBJavaScriptView Raw
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3import { HttpHeaders } from "@azure/core-http";
4import { HTTP_VERSION_1_1, HTTP_LINE_ENDING, HeaderConstants, HTTPURLConnection, } from "./utils/constants";
5import { getBodyAsText } from "./BatchUtils";
6import { logger } from "./log";
7const HTTP_HEADER_DELIMITER = ": ";
8const SPACE_DELIMITER = " ";
9const NOT_FOUND = -1;
10/**
11 * Util class for parsing batch response.
12 */
13export class BatchResponseParser {
14 constructor(batchResponse, subRequests) {
15 if (!batchResponse || !batchResponse.contentType) {
16 // In special case(reported), server may return invalid content-type which could not be parsed.
17 throw new RangeError("batchResponse is malformed or doesn't contain valid content-type.");
18 }
19 if (!subRequests || subRequests.size === 0) {
20 // This should be prevent during coding.
21 throw new RangeError("Invalid state: subRequests is not provided or size is 0.");
22 }
23 this.batchResponse = batchResponse;
24 this.subRequests = subRequests;
25 this.responseBatchBoundary = this.batchResponse.contentType.split("=")[1];
26 this.perResponsePrefix = `--${this.responseBatchBoundary}${HTTP_LINE_ENDING}`;
27 this.batchResponseEnding = `--${this.responseBatchBoundary}--`;
28 }
29 // For example of response, please refer to https://docs.microsoft.com/en-us/rest/api/storageservices/blob-batch#response
30 async parseBatchResponse() {
31 // When logic reach here, suppose batch request has already succeeded with 202, so we can further parse
32 // sub request's response.
33 if (this.batchResponse._response.status !== HTTPURLConnection.HTTP_ACCEPTED) {
34 throw new Error(`Invalid state: batch request failed with status: '${this.batchResponse._response.status}'.`);
35 }
36 const responseBodyAsText = await getBodyAsText(this.batchResponse);
37 const subResponses = responseBodyAsText
38 .split(this.batchResponseEnding)[0] // string after ending is useless
39 .split(this.perResponsePrefix)
40 .slice(1); // string before first response boundary is useless
41 const subResponseCount = subResponses.length;
42 // Defensive coding in case of potential error parsing.
43 // Note: subResponseCount == 1 is special case where sub request is invalid.
44 // We try to prevent such cases through early validation, e.g. validate sub request count >= 1.
45 // While in unexpected sub request invalid case, we allow sub response to be parsed and return to user.
46 if (subResponseCount !== this.subRequests.size && subResponseCount !== 1) {
47 throw new Error("Invalid state: sub responses' count is not equal to sub requests' count.");
48 }
49 const deserializedSubResponses = new Array(subResponseCount);
50 let subResponsesSucceededCount = 0;
51 let subResponsesFailedCount = 0;
52 // Parse sub subResponses.
53 for (let index = 0; index < subResponseCount; index++) {
54 const subResponse = subResponses[index];
55 const deserializedSubResponse = {};
56 deserializedSubResponse.headers = new HttpHeaders();
57 const responseLines = subResponse.split(`${HTTP_LINE_ENDING}`);
58 let subRespHeaderStartFound = false;
59 let subRespHeaderEndFound = false;
60 let subRespFailed = false;
61 let contentId = NOT_FOUND;
62 for (const responseLine of responseLines) {
63 if (!subRespHeaderStartFound) {
64 // Convention line to indicate content ID
65 if (responseLine.startsWith(HeaderConstants.CONTENT_ID)) {
66 contentId = parseInt(responseLine.split(HTTP_HEADER_DELIMITER)[1]);
67 }
68 // Http version line with status code indicates the start of sub request's response.
69 // Example: HTTP/1.1 202 Accepted
70 if (responseLine.startsWith(HTTP_VERSION_1_1)) {
71 subRespHeaderStartFound = true;
72 const tokens = responseLine.split(SPACE_DELIMITER);
73 deserializedSubResponse.status = parseInt(tokens[1]);
74 deserializedSubResponse.statusMessage = tokens.slice(2).join(SPACE_DELIMITER);
75 }
76 continue; // Skip convention headers not specifically for sub request i.e. Content-Type: application/http and Content-ID: *
77 }
78 if (responseLine.trim() === "") {
79 // Sub response's header start line already found, and the first empty line indicates header end line found.
80 if (!subRespHeaderEndFound) {
81 subRespHeaderEndFound = true;
82 }
83 continue; // Skip empty line
84 }
85 // Note: when code reach here, it indicates subRespHeaderStartFound == true
86 if (!subRespHeaderEndFound) {
87 if (responseLine.indexOf(HTTP_HEADER_DELIMITER) === -1) {
88 // Defensive coding to prevent from missing valuable lines.
89 throw new Error(`Invalid state: find non-empty line '${responseLine}' without HTTP header delimiter '${HTTP_HEADER_DELIMITER}'.`);
90 }
91 // Parse headers of sub response.
92 const tokens = responseLine.split(HTTP_HEADER_DELIMITER);
93 deserializedSubResponse.headers.set(tokens[0], tokens[1]);
94 if (tokens[0] === HeaderConstants.X_MS_ERROR_CODE) {
95 deserializedSubResponse.errorCode = tokens[1];
96 subRespFailed = true;
97 }
98 }
99 else {
100 // Assemble body of sub response.
101 if (!deserializedSubResponse.bodyAsText) {
102 deserializedSubResponse.bodyAsText = "";
103 }
104 deserializedSubResponse.bodyAsText += responseLine;
105 }
106 } // Inner for end
107 // The response will contain the Content-ID header for each corresponding subrequest response to use for tracking.
108 // The Content-IDs are set to a valid index in the subrequests we sent. In the status code 202 path, we could expect it
109 // to be 1-1 mapping from the [0, subRequests.size) to the Content-IDs returned. If not, we simply don't return that
110 // unexpected subResponse in the parsed reponse and we can always look it up in the raw response for debugging purpose.
111 if (contentId !== NOT_FOUND &&
112 Number.isInteger(contentId) &&
113 contentId >= 0 &&
114 contentId < this.subRequests.size &&
115 deserializedSubResponses[contentId] === undefined) {
116 deserializedSubResponse._request = this.subRequests.get(contentId);
117 deserializedSubResponses[contentId] = deserializedSubResponse;
118 }
119 else {
120 logger.error(`subResponses[${index}] is dropped as the Content-ID is not found or invalid, Content-ID: ${contentId}`);
121 }
122 if (subRespFailed) {
123 subResponsesFailedCount++;
124 }
125 else {
126 subResponsesSucceededCount++;
127 }
128 }
129 return {
130 subResponses: deserializedSubResponses,
131 subResponsesSucceededCount: subResponsesSucceededCount,
132 subResponsesFailedCount: subResponsesFailedCount,
133 };
134 }
135}
136//# sourceMappingURL=BatchResponseParser.js.map
\No newline at end of file