Skip to content
16 changes: 14 additions & 2 deletions packages/typespec-ts/src/transform/transformParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
getSchemaForType,
getTypeName,
isArrayType,
isBodyRequired
isBodyRequired,
isByteOrByteUnion
} from "../utils/modelUtils.js";
import {
getOperationGroupName,
Expand Down Expand Up @@ -302,7 +303,18 @@ function transformRequestBody(
importedModels: Set<string>,
headers: ParameterMetadata[]
) {
const contentTypes = extractMediaTypes(parameters.body?.contentTypes ?? []);
const rawContentTypes = parameters.body?.contentTypes ?? [];
let contentTypes = extractMediaTypes(rawContentTypes);

// Treat */* as binary when the body is a bytes type
if (
rawContentTypes.some((ct) => ct === "*/*") &&
isByteOrByteUnion(dpgContext, bodyType) &&
!contentTypes.includes(KnownMediaType.Binary)
) {
contentTypes = [...contentTypes, KnownMediaType.Binary];
}
Comment thread
kazrael2119 marked this conversation as resolved.

const schema = getSchemaForType(dpgContext, bodyType, {
mediaTypes: contentTypes,
isRequestBody: true,
Expand Down
16 changes: 12 additions & 4 deletions packages/typespec-ts/src/utils/operationUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,19 @@ export function isBinaryPayload(
body: Type,
contentType: string | string[]
) {
const knownMediaTypes: KnownMediaType[] = (
Array.isArray(contentType) ? contentType : [contentType]
).map((ct) => knownMediaType(ct));
const contentTypes = Array.isArray(contentType) ? contentType : [contentType];
const isBytes = body ? isByteOrByteUnion(dpgContext, body) : false;

// Treat */* as binary when the body is a bytes type
if (contentTypes.some((ct) => ct === "*/*") && isBytes) {
return true;
}

const knownMediaTypes: KnownMediaType[] = contentTypes.map((ct) =>
knownMediaType(ct)
);
for (const type of knownMediaTypes) {
if (type === KnownMediaType.Binary && isByteOrByteUnion(dpgContext, body)) {
if (type === KnownMediaType.Binary || isBytes) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# bytes response with */* content type should be treated as binary

When a response has `*/*` content type and a `bytes` body, `isBinaryPayload` should return
`true` so the response is deserialized as `Uint8Array` (binary) rather than `string` (base64).

## TypeSpec

```tsp
@post op uploadFile(
@body body: string
): {
@header contentType: "*/*",
@body body: bytes
};
```

## Operations

```ts operations
import { TestingContext as Client } from "./index.js";
import { getBinaryResponse } from "../static-helpers/serialization/get-binary-response.js";
import { UploadFileOptionalParams } from "./options.js";
import {
StreamableMethod,
PathUncheckedResponse,
createRestError,
operationOptionsToRequestParameters,
} from "@azure-rest/core-client";

export function _uploadFileSend(
context: Client,
body: string,
options: UploadFileOptionalParams = { requestOptions: {} },
): StreamableMethod {
return context
.path("/")
.post({
...operationOptionsToRequestParameters(options),
contentType: "text/plain",
headers: { accept: "*/*", ...options.requestOptions?.headers },
body: body,
});
}

export async function _uploadFileDeserialize(result: PathUncheckedResponse): Promise<Uint8Array> {
const expectedStatuses = ["200"];
if (!expectedStatuses.includes(result.status)) {
throw createRestError(result);
}

return result.body;
}

export async function uploadFile(
context: Client,
body: string,
options: UploadFileOptionalParams = { requestOptions: {} },
): Promise<Uint8Array> {
const streamableMethod = _uploadFileSend(context, body, options);
const result = await getBinaryResponse(streamableMethod);
return _uploadFileDeserialize(result);
}
```
5 changes: 3 additions & 2 deletions packages/typespec-ts/test/unit/responsesGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ describe("Responses.ts", () => {
`
);
});
it("@header contentType text/plain should keep format to byte(finally string)", async () => {
it("@header contentType text/plain with bytes body should be treated as binary", async () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why current change will impact rlc ut?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isBinaryPayload is shared between the RLC (transformResponses.ts:176) and modular code paths. With ||, the loop condition becomes type === KnownMediaType.Binary || isBytes — so for text/plain + bytes body, knownMediaType("text/plain") returns KnownMediaType.Text (not Binary), but isBytes is true, making the whole expression true. That causes text/plain + bytes to be treated as binary (producing Uint8Array) in both RLC and modular paths, which is why the existing RLC test needed to be updated.

This is intentional and consistent with the reviewer's original || suggestion — when the body is declared as bytes, it should be treated as binary data regardless of the content type label.

const responses = await emitResponsesFromTypeSpec(`
@get op read(): {@header contentType: "text/plain", @body body: bytes};
`);
Expand All @@ -232,7 +232,8 @@ describe("Responses.ts", () => {
/** The request has succeeded. */
export interface Read200Response extends HttpResponse {
status: "200";
body: string;
/** Value may contain any sequence of octets */
body: Uint8Array;
headers: RawHttpHeaders & Read200Headers;
}
`
Expand Down
Loading