Skip to content

Commit ac1a26b

Browse files
joherediCopilotJialinHuang803jeremymeng
authored
Fix: Restore full error.details handling for binary wrap-non-model-return deserializers (#3852)
* fix: restore full error.details handling for binary wrap-non-model-return deserializers The wrap-non-model-return feature (commit 49500ef) introduced early returns in the binary wrap path that skipped status checking and error.details extraction in generated deserialize functions. This fix: - Adds a new getBinaryStream static helper that separates stream extraction from error handling (Node + browser variants) - Removes the status check skip for binary wrap - all operations now generate the full getExceptionThrowStatement with error.details, exception headers, and restErrorCodeHeader support - The deserializer parameter is renamed to _streamableResult for binary wrap, with getBinaryStream resolving it into an HttpResponse named result so existing error handling code references work unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add error enrichment scenarios for binary wrap-non-model-return Adds two new unit test scenarios verifying that binary wrap deserializers generate full error.details handling: - Binary wrap + error model: verifies error.details = apiErrorDeserializer(result.body) - Binary wrap + error model + exception headers: verifies error.details with exception header merging via _getBlobDeserializeExceptionHeaders(result) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add storage-compat scenario for void+headers with error enrichment Verifies that when enable-storage-compat and include-headers-in-response are both enabled on a void-body operation with response headers and an error model: 1. Deserializer generates error.details = storageErrorDeserializer(result.body) 2. Exception headers are merged into error.details 3. Operation function extracts parsedHeaders and passes them to addStorageCompatResponse Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update smoke tests * Update for Storage Compat binary response - move the `getBinaryStream()` call out of `_operationDeserialize()` and into the operation method `operation()`, where its result is passed to both `_opDeserialize()` and `_operationDeserializeHeaders()` - `_operationDeserialize()` now takes the result of `getBinaryStream()`, process and throw for error response if any; return the wrapped stream body. - return type is fixed for wrapped operation. * update smoke test * attempt to fix missing import of OperationResponse * react to recent fast-xml-parser change * Remove unnecessary logic and rename the helper --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jialin Huang <jialinhuang@microsoft.com> Co-authored-by: Jeremy Meng <jeremy.ymeng@gmail.com>
1 parent 738b13e commit ac1a26b

20 files changed

Lines changed: 516 additions & 220 deletions

File tree

packages/typespec-test/test/batch_modular/generated/typespec-ts/src/api/operations.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ import {
113113
PagedAsyncIterableIterator,
114114
buildPagedAsyncIterator,
115115
} from "../static-helpers/pagingHelpers.js";
116-
import { getBinaryResponseBody } from "../static-helpers/serialization/get-binary-response-body.js";
116+
import { getBinaryStreamResponse } from "../static-helpers/serialization/get-binary-stream-response.js";
117117
import { expandUrlTemplate } from "../static-helpers/urlTemplate.js";
118118
import {
119119
ListNodeFilesOptionalParams,
@@ -410,9 +410,17 @@ export function _getNodeFileSend(
410410
}
411411

412412
export async function _getNodeFileDeserialize(
413-
result: StreamableMethod,
413+
result: PathUncheckedResponse & GetNodeFileResponse,
414414
): Promise<GetNodeFileResponse> {
415-
return getBinaryResponseBody(result, ["200"], batchErrorDeserializer);
415+
const expectedStatuses = ["200"];
416+
if (!expectedStatuses.includes(result.status)) {
417+
const error = createRestError(result);
418+
error.details = batchErrorDeserializer(result.body);
419+
420+
throw error;
421+
}
422+
423+
return { blobBody: result.blobBody, readableStreamBody: result.readableStreamBody };
416424
}
417425

418426
/** Returns the content of the specified Compute Node file. */
@@ -424,7 +432,8 @@ export async function getNodeFile(
424432
options: GetNodeFileOptionalParams = { requestOptions: {} },
425433
): Promise<GetNodeFileResponse> {
426434
const streamableMethod = _getNodeFileSend(context, poolId, nodeId, filePath, options);
427-
return _getNodeFileDeserialize(streamableMethod);
435+
const result = await getBinaryStreamResponse(streamableMethod);
436+
return _getNodeFileDeserialize(result);
428437
}
429438

430439
export function _deleteNodeFileSend(
@@ -822,9 +831,17 @@ export function _getNodeRemoteDesktopFileSend(
822831
}
823832

824833
export async function _getNodeRemoteDesktopFileDeserialize(
825-
result: StreamableMethod,
834+
result: PathUncheckedResponse & GetNodeRemoteDesktopFileResponse,
826835
): Promise<GetNodeRemoteDesktopFileResponse> {
827-
return getBinaryResponseBody(result, ["200"], batchErrorDeserializer);
836+
const expectedStatuses = ["200"];
837+
if (!expectedStatuses.includes(result.status)) {
838+
const error = createRestError(result);
839+
error.details = batchErrorDeserializer(result.body);
840+
841+
throw error;
842+
}
843+
844+
return { blobBody: result.blobBody, readableStreamBody: result.readableStreamBody };
828845
}
829846

830847
/**
@@ -840,7 +857,8 @@ export async function getNodeRemoteDesktopFile(
840857
options: GetNodeRemoteDesktopFileOptionalParams = { requestOptions: {} },
841858
): Promise<GetNodeRemoteDesktopFileResponse> {
842859
const streamableMethod = _getNodeRemoteDesktopFileSend(context, poolId, nodeId, options);
843-
return _getNodeRemoteDesktopFileDeserialize(streamableMethod);
860+
const result = await getBinaryStreamResponse(streamableMethod);
861+
return _getNodeRemoteDesktopFileDeserialize(result);
844862
}
845863

846864
export function _getNodeRemoteLoginSettingsSend(
@@ -1655,9 +1673,17 @@ export function _getTaskFileSend(
16551673
}
16561674

16571675
export async function _getTaskFileDeserialize(
1658-
result: StreamableMethod,
1676+
result: PathUncheckedResponse & GetTaskFileResponse,
16591677
): Promise<GetTaskFileResponse> {
1660-
return getBinaryResponseBody(result, ["200"], batchErrorDeserializer);
1678+
const expectedStatuses = ["200"];
1679+
if (!expectedStatuses.includes(result.status)) {
1680+
const error = createRestError(result);
1681+
error.details = batchErrorDeserializer(result.body);
1682+
1683+
throw error;
1684+
}
1685+
1686+
return { blobBody: result.blobBody, readableStreamBody: result.readableStreamBody };
16611687
}
16621688

16631689
/** Returns the content of the specified Task file. */
@@ -1669,7 +1695,8 @@ export async function getTaskFile(
16691695
options: GetTaskFileOptionalParams = { requestOptions: {} },
16701696
): Promise<GetTaskFileResponse> {
16711697
const streamableMethod = _getTaskFileSend(context, jobId, taskId, filePath, options);
1672-
return _getTaskFileDeserialize(streamableMethod);
1698+
const result = await getBinaryStreamResponse(streamableMethod);
1699+
return _getTaskFileDeserialize(result);
16731700
}
16741701

16751702
export function _deleteTaskFileSend(

packages/typespec-test/test/batch_modular/generated/typespec-ts/src/static-helpers/serialization/get-binary-response-body-browser.mts

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/typespec-test/test/batch_modular/generated/typespec-ts/src/static-helpers/serialization/get-binary-response-body.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { HttpResponse, StreamableMethod } from "@azure-rest/core-client";
2+
3+
/**
4+
* Resolves a StreamableMethod into a binary stream response using browser streaming.
5+
* Returns both the raw HttpResponse (for status/header inspection) and a blobBody Promise.
6+
* Error handling is left to the caller so that generated deserializers can apply
7+
* operation-specific error deserialization (per-status-code details, exception headers, etc.).
8+
*/
9+
export async function getBinaryStreamResponse(
10+
streamableMethod: StreamableMethod
11+
): Promise<
12+
HttpResponse & {
13+
blobBody?: Promise<Blob>;
14+
readableStreamBody?: NodeJS.ReadableStream;
15+
}
16+
> {
17+
const response = await streamableMethod.asBrowserStream();
18+
return {
19+
...response,
20+
blobBody: new Response(response.body).blob(),
21+
readableStreamBody: undefined,
22+
};
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { HttpResponse, StreamableMethod } from "@azure-rest/core-client";
5+
6+
/**
7+
* Resolves a StreamableMethod into a binary stream response using Node.js streaming.
8+
* Returns both the raw HttpResponse (for status/header inspection) and the readable stream body.
9+
* Error handling is left to the caller so that generated deserializers can apply
10+
* operation-specific error deserialization (per-status-code details, exception headers, etc.).
11+
*/
12+
export async function getBinaryStreamResponse(streamableMethod: StreamableMethod): Promise<
13+
HttpResponse & {
14+
blobBody?: Promise<Blob>;
15+
readableStreamBody?: NodeJS.ReadableStream;
16+
}
17+
> {
18+
const response = await streamableMethod.asNodeStream();
19+
return {
20+
...response,
21+
blobBody: undefined,
22+
readableStreamBody: response.body,
23+
};
24+
}

packages/typespec-test/test/openai_modular/generated/typespec-ts/src/api/operations.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
GetAudioTranslationAsPlainTextResponse,
3434
GetAudioTranscriptionAsPlainTextResponse,
3535
} from "../models/models.js";
36-
import { getBinaryResponseBody } from "../static-helpers/serialization/get-binary-response-body.js";
36+
import { getBinaryStreamResponse } from "../static-helpers/serialization/get-binary-stream-response.js";
3737
import { expandUrlTemplate } from "../static-helpers/urlTemplate.js";
3838
import {
3939
GetEmbeddingsOptionalParams,
@@ -128,9 +128,14 @@ export function _generateSpeechFromTextSend(
128128
}
129129

130130
export async function _generateSpeechFromTextDeserialize(
131-
result: StreamableMethod,
131+
result: PathUncheckedResponse & GenerateSpeechFromTextResponse,
132132
): Promise<GenerateSpeechFromTextResponse> {
133-
return getBinaryResponseBody(result, ["200"]);
133+
const expectedStatuses = ["200"];
134+
if (!expectedStatuses.includes(result.status)) {
135+
throw createRestError(result);
136+
}
137+
138+
return { blobBody: result.blobBody, readableStreamBody: result.readableStreamBody };
134139
}
135140

136141
/** Generates text-to-speech audio from the input text. */
@@ -141,7 +146,8 @@ export async function generateSpeechFromText(
141146
options: GenerateSpeechFromTextOptionalParams = { requestOptions: {} },
142147
): Promise<GenerateSpeechFromTextResponse> {
143148
const streamableMethod = _generateSpeechFromTextSend(context, deploymentId, body, options);
144-
return _generateSpeechFromTextDeserialize(streamableMethod);
149+
const result = await getBinaryStreamResponse(streamableMethod);
150+
return _generateSpeechFromTextDeserialize(result);
145151
}
146152

147153
export function _getImageGenerationsSend(

packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/serialization/get-binary-response-body-browser.mts

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/serialization/get-binary-response-body.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { HttpResponse, StreamableMethod } from "@azure-rest/core-client";
2+
3+
/**
4+
* Resolves a StreamableMethod into a binary stream response using browser streaming.
5+
* Returns both the raw HttpResponse (for status/header inspection) and a blobBody Promise.
6+
* Error handling is left to the caller so that generated deserializers can apply
7+
* operation-specific error deserialization (per-status-code details, exception headers, etc.).
8+
*/
9+
export async function getBinaryStreamResponse(
10+
streamableMethod: StreamableMethod
11+
): Promise<
12+
HttpResponse & {
13+
blobBody?: Promise<Blob>;
14+
readableStreamBody?: NodeJS.ReadableStream;
15+
}
16+
> {
17+
const response = await streamableMethod.asBrowserStream();
18+
return {
19+
...response,
20+
blobBody: new Response(response.body).blob(),
21+
readableStreamBody: undefined,
22+
};
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { HttpResponse, StreamableMethod } from "@azure-rest/core-client";
5+
6+
/**
7+
* Resolves a StreamableMethod into a binary stream response using Node.js streaming.
8+
* Returns both the raw HttpResponse (for status/header inspection) and the readable stream body.
9+
* Error handling is left to the caller so that generated deserializers can apply
10+
* operation-specific error deserialization (per-status-code details, exception headers, etc.).
11+
*/
12+
export async function getBinaryStreamResponse(streamableMethod: StreamableMethod): Promise<
13+
HttpResponse & {
14+
blobBody?: Promise<Blob>;
15+
readableStreamBody?: NodeJS.ReadableStream;
16+
}
17+
> {
18+
const response = await streamableMethod.asNodeStream();
19+
return {
20+
...response,
21+
blobBody: undefined,
22+
readableStreamBody: response.body,
23+
};
24+
}

0 commit comments

Comments
 (0)