Skip to content

Commit e140079

Browse files
authored
[core-http] use response status to determine whether body is stream (Azure#13192)
Our current code has the incorrect assumption that if any body mapper of an operation spec is of `Stream` type then response body must be a stream and `httpRequest.streamResponseBody` is set according to this assumption. However, Storage issue Azure#12997 shows that the default response of Download Blob operation does not have a stream body with the following operation spec: ```typescript responses: { 200: { bodyMapper: { serializedName: "parsedResponse", type: { name: "Stream" } }, headersMapper: Mappers.BlobDownloadHeaders }, ... default: { bodyMapper: Mappers.StorageError, headersMapper: Mappers.BlobDownloadHeaders } }, ``` Treating response body as stream in the default case prevent us from parsing the body which leads to missing `code` and `message` from the error object. This change fixes the issue by actually checking the body type of the response corresponding to the response status code. WebResource{Like}.streamResponseBody is deprecated because a boolean value is not enough to determine whether a response is streaming or not. A list of streaming response status code `streamResponseStatusCodes` is introduced and used instead. ServiceClient no longer sets the deprecated `streamResponsebody` property based on incorrect assumption. However we still respect `streamResponseBody` in case it's set by users. Core v2 removes `streamResponseBody`.
1 parent 5864021 commit e140079

23 files changed

+743
-75
lines changed

sdk/core/core-client/src/deserializationPolicy.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
RequiredSerializerOptions
2020
} from "./interfaces";
2121
import { MapperTypeNames } from "./serializer";
22-
import { isStreamOperation } from "./interfaceHelpers";
2322
import { getOperationRequestInfo } from "./operationHelpers";
2423

2524
const defaultJsonContentTypes = ["application/json", "text/json"];
@@ -243,7 +242,9 @@ function handleErrorResponse(
243242

244243
const errorResponseSpec = responseSpec ?? operationSpec.responses.default;
245244

246-
const initialErrorMessage = isStreamOperation(operationSpec)
245+
const initialErrorMessage = parsedResponse.request.streamResponseStatusCodes?.has(
246+
parsedResponse.status
247+
)
247248
? `Unexpected status code: ${parsedResponse.status}`
248249
: (parsedResponse.bodyAsText as string);
249250

@@ -318,7 +319,10 @@ async function parse(
318319
opts: RequiredSerializerOptions,
319320
parseXML?: (str: string, opts?: XmlOptions) => Promise<any>
320321
): Promise<FullOperationResponse> {
321-
if (!operationResponse.request.streamResponseBody && operationResponse.bodyAsText) {
322+
if (
323+
!operationResponse.request.streamResponseStatusCodes?.has(operationResponse.status) &&
324+
operationResponse.bodyAsText
325+
) {
322326
const text = operationResponse.bodyAsText;
323327
const contentType: string = operationResponse.headers.get("Content-Type") || "";
324328
const contentComponents: string[] = !contentType

sdk/core/core-client/src/interfaceHelpers.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ import { MapperTypeNames } from "./serializer";
55
import { OperationSpec, OperationParameter } from "./interfaces";
66

77
/**
8+
* Gets the list of status codes for streaming responses.
89
* @internal @hidden
910
*/
10-
export function isStreamOperation(operationSpec: OperationSpec): boolean {
11+
export function getStreamingResponseStatusCodes(operationSpec: OperationSpec): Set<number> {
12+
const result = new Set<number>();
1113
for (const statusCode in operationSpec.responses) {
1214
const operationResponse = operationSpec.responses[statusCode];
1315
if (
1416
operationResponse.bodyMapper &&
1517
operationResponse.bodyMapper.type.name === MapperTypeNames.Stream
1618
) {
17-
return true;
19+
result.add(Number(statusCode));
1820
}
1921
}
20-
return false;
22+
return result;
2123
}
2224

2325
/**

sdk/core/core-client/src/serviceClient.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
CompositeMapper,
2222
XmlOptions
2323
} from "./interfaces";
24-
import { isStreamOperation } from "./interfaceHelpers";
24+
import { getStreamingResponseStatusCodes } from "./interfaceHelpers";
2525
import { getRequestUrl } from "./urlHelpers";
2626
import { isPrimitiveType } from "./utils";
2727
import { deserializationPolicy, DeserializationPolicyOptions } from "./deserializationPolicy";
@@ -195,8 +195,8 @@ export class ServiceClient {
195195
}
196196
}
197197

198-
if (request.streamResponseBody === undefined) {
199-
request.streamResponseBody = isStreamOperation(operationSpec);
198+
if (request.streamResponseStatusCodes === undefined) {
199+
request.streamResponseStatusCodes = getStreamingResponseStatusCodes(operationSpec);
200200
}
201201

202202
try {

sdk/core/core-client/test/serviceClient.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,86 @@ describe("ServiceClient", function() {
821821
}
822822
});
823823

824+
it("should deserialize non-streaming default response", async function() {
825+
const StorageError: CompositeMapper = {
826+
serializedName: "StorageError",
827+
type: {
828+
name: "Composite",
829+
className: "StorageError",
830+
modelProperties: {
831+
message: {
832+
xmlName: "Message",
833+
serializedName: "Message",
834+
type: {
835+
name: "String"
836+
}
837+
},
838+
code: {
839+
xmlName: "Code",
840+
serializedName: "Code",
841+
type: {
842+
name: "String"
843+
}
844+
}
845+
}
846+
}
847+
};
848+
849+
const serializer = createSerializer(undefined, true);
850+
851+
const operationSpec: OperationSpec = {
852+
httpMethod: "GET",
853+
responses: {
854+
200: {
855+
bodyMapper: {
856+
serializedName: "parsedResponse",
857+
type: {
858+
name: "Stream"
859+
}
860+
}
861+
},
862+
default: {
863+
bodyMapper: StorageError
864+
}
865+
},
866+
baseUrl: "https://example.com",
867+
serializer
868+
};
869+
870+
let request: OperationRequest = createPipelineRequest({ url: "https://example.com" });
871+
const operationInfo = getOperationRequestInfo(request);
872+
operationInfo.operationSpec = operationSpec;
873+
874+
const httpsClient: HttpsClient = {
875+
sendRequest: (req) => {
876+
request = req;
877+
return Promise.resolve({
878+
request,
879+
status: 500,
880+
headers: createHttpHeaders({
881+
"Content-Type": "application/json"
882+
}),
883+
bodyAsText: `{ "Code": "BlobNotFound", "Message": "The specified blob does not exist." }`
884+
});
885+
}
886+
};
887+
888+
const pipeline = createEmptyPipeline();
889+
pipeline.addPolicy(deserializationPolicy());
890+
const client = new ServiceClient({
891+
httpsClient,
892+
pipeline
893+
});
894+
895+
try {
896+
await client.sendOperationRequest({}, operationSpec);
897+
assert.fail();
898+
} catch (ex) {
899+
assert.strictEqual(ex.code, "BlobNotFound");
900+
assert.strictEqual(ex.message, "The specified blob does not exist.");
901+
}
902+
});
903+
824904
it("should re-use the common instance of DefaultHttpClient", function() {
825905
const client = new ServiceClient();
826906
assert.strictEqual((client as any)._httpsClient, getCachedDefaultHttpsClient());

sdk/core/core-http/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 1.2.3 (Unreleased)
44

5+
- Fix an issue where non-streaming response body was treated as stream [PR 13192](https://github.com/Azure/azure-sdk-for-js/pull/13192)
56

67
## 1.2.2 (2021-01-07)
78

sdk/core/core-http/review/core-http.api.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ export class WebResource implements WebResourceLike {
901901
[key: string]: any;
902902
}, headers?: {
903903
[key: string]: any;
904-
} | HttpHeadersLike, streamResponseBody?: boolean, withCredentials?: boolean, abortSignal?: AbortSignalLike, timeout?: number, onUploadProgress?: (progress: TransferProgressEvent) => void, onDownloadProgress?: (progress: TransferProgressEvent) => void, proxySettings?: ProxySettings, keepAlive?: boolean, decompressResponse?: boolean);
904+
} | HttpHeadersLike, streamResponseBody?: boolean, withCredentials?: boolean, abortSignal?: AbortSignalLike, timeout?: number, onUploadProgress?: (progress: TransferProgressEvent) => void, onDownloadProgress?: (progress: TransferProgressEvent) => void, proxySettings?: ProxySettings, keepAlive?: boolean, decompressResponse?: boolean, streamResponseStatusCodes?: Set<number>);
905905
// (undocumented)
906906
abortSignal?: AbortSignalLike;
907907
// (undocumented)
@@ -932,7 +932,9 @@ export class WebResource implements WebResourceLike {
932932
requestId: string;
933933
shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean);
934934
spanOptions?: SpanOptions;
935+
// @deprecated (undocumented)
935936
streamResponseBody?: boolean;
937+
streamResponseStatusCodes?: Set<number>;
936938
// (undocumented)
937939
timeout: number;
938940
// (undocumented)
@@ -965,7 +967,9 @@ export interface WebResourceLike {
965967
requestId: string;
966968
shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean);
967969
spanOptions?: SpanOptions;
970+
// @deprecated (undocumented)
968971
streamResponseBody?: boolean;
972+
streamResponseStatusCodes?: Set<number>;
969973
timeout: number;
970974
url: string;
971975
validateRequestProperties(): void;

sdk/core/core-http/src/fetchHttpClient.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,19 @@ export abstract class FetchHttpClient implements HttpClient {
155155
const response: CommonResponse = await this.fetch(httpRequest.url, requestInit);
156156

157157
const headers = parseHeaders(response.headers);
158+
159+
const streaming =
160+
httpRequest.streamResponseStatusCodes?.has(response.status) ||
161+
httpRequest.streamResponseBody;
162+
158163
operationResponse = {
159164
headers: headers,
160165
request: httpRequest,
161166
status: response.status,
162-
readableStreamBody: httpRequest.streamResponseBody
167+
readableStreamBody: streaming
163168
? ((response.body as unknown) as NodeJS.ReadableStream)
164169
: undefined,
165-
bodyAsText: !httpRequest.streamResponseBody ? await response.text() : undefined
170+
bodyAsText: !streaming ? await response.text() : undefined
166171
};
167172

168173
const onDownloadProgress = httpRequest.onDownloadProgress;

sdk/core/core-http/src/operationSpec.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,19 @@ export interface OperationSpec {
9595
readonly responses: { [responseCode: string]: OperationResponse };
9696
}
9797

98-
export function isStreamOperation(operationSpec: OperationSpec): boolean {
99-
let result = false;
98+
/**
99+
* Gets the list of status codes for streaming responses.
100+
* @internal @hidden
101+
*/
102+
export function getStreamResponseStatusCodes(operationSpec: OperationSpec): Set<number> {
103+
const result = new Set<number>();
100104
for (const statusCode in operationSpec.responses) {
101-
const operationResponse: OperationResponse = operationSpec.responses[statusCode];
105+
const operationResponse = operationSpec.responses[statusCode];
102106
if (
103107
operationResponse.bodyMapper &&
104108
operationResponse.bodyMapper.type.name === MapperType.Stream
105109
) {
106-
result = true;
107-
break;
110+
result.add(Number(statusCode));
108111
}
109112
}
110113
return result;

sdk/core/core-http/src/policies/deserializationPolicy.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { HttpOperationResponse } from "../httpOperationResponse";
55
import { OperationResponse } from "../operationResponse";
6-
import { OperationSpec, isStreamOperation } from "../operationSpec";
6+
import { OperationSpec } from "../operationSpec";
77
import { RestError } from "../restError";
88
import { MapperType } from "../serializer";
99
import { parseXML } from "../util/xml";
@@ -256,7 +256,10 @@ function handleErrorResponse(
256256
}
257257

258258
const errorResponseSpec = responseSpec ?? operationSpec.responses.default;
259-
const initialErrorMessage = isStreamOperation(operationSpec)
259+
const streaming =
260+
parsedResponse.request.streamResponseStatusCodes?.has(parsedResponse.status) ||
261+
parsedResponse.request.streamResponseBody;
262+
const initialErrorMessage = streaming
260263
? `Unexpected status code: ${parsedResponse.status}`
261264
: (parsedResponse.bodyAsText as string);
262265

@@ -341,7 +344,10 @@ function parse(
341344
return Promise.reject(e);
342345
};
343346

344-
if (!operationResponse.request.streamResponseBody && operationResponse.bodyAsText) {
347+
const streaming =
348+
operationResponse.request.streamResponseStatusCodes?.has(operationResponse.status) ||
349+
operationResponse.request.streamResponseBody;
350+
if (!streaming && operationResponse.bodyAsText) {
345351
const text = operationResponse.bodyAsText;
346352
const contentType: string = operationResponse.headers.get("Content-Type") || "";
347353
const contentComponents: string[] = !contentType

sdk/core/core-http/src/serviceClient.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
OperationParameter,
1414
ParameterPath
1515
} from "./operationParameter";
16-
import { isStreamOperation, OperationSpec } from "./operationSpec";
16+
import { getStreamResponseStatusCodes, OperationSpec } from "./operationSpec";
1717
import {
1818
deserializationPolicy,
1919
DeserializationContentTypes,
@@ -504,8 +504,8 @@ export class ServiceClient {
504504

505505
serializeRequestBody(this, httpRequest, operationArguments, operationSpec);
506506

507-
if (httpRequest.streamResponseBody === undefined || httpRequest.streamResponseBody === null) {
508-
httpRequest.streamResponseBody = isStreamOperation(operationSpec);
507+
if (httpRequest.streamResponseStatusCodes === undefined) {
508+
httpRequest.streamResponseStatusCodes = getStreamResponseStatusCodes(operationSpec);
509509
}
510510

511511
let rawResponse: HttpOperationResponse;

0 commit comments

Comments
 (0)