diff --git a/packages/typespec-ts/src/transform/transformParameters.ts b/packages/typespec-ts/src/transform/transformParameters.ts index b70e415b43..87464f8ec9 100644 --- a/packages/typespec-ts/src/transform/transformParameters.ts +++ b/packages/typespec-ts/src/transform/transformParameters.ts @@ -36,7 +36,8 @@ import { getSchemaForType, getTypeName, isArrayType, - isBodyRequired + isBodyRequired, + isByteOrByteUnion } from "../utils/modelUtils.js"; import { getOperationGroupName, @@ -302,7 +303,18 @@ function transformRequestBody( importedModels: Set, 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]; + } + const schema = getSchemaForType(dpgContext, bodyType, { mediaTypes: contentTypes, isRequestBody: true, diff --git a/packages/typespec-ts/src/utils/operationUtil.ts b/packages/typespec-ts/src/utils/operationUtil.ts index 3ad9903b1d..b8dcef18ca 100644 --- a/packages/typespec-ts/src/utils/operationUtil.ts +++ b/packages/typespec-ts/src/utils/operationUtil.ts @@ -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; } } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/bodyParam/bytesWithWildcardContentType.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/bodyParam/bytesWithWildcardContentType.md new file mode 100644 index 0000000000..43be23f6ed --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/bodyParam/bytesWithWildcardContentType.md @@ -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 { + 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 { + const streamableMethod = _uploadFileSend(context, body, options); + const result = await getBinaryResponse(streamableMethod); + return _uploadFileDeserialize(result); +} +``` diff --git a/packages/typespec-ts/test/unit/responsesGenerator.spec.ts b/packages/typespec-ts/test/unit/responsesGenerator.spec.ts index fd84d19fa9..7be19e8112 100644 --- a/packages/typespec-ts/test/unit/responsesGenerator.spec.ts +++ b/packages/typespec-ts/test/unit/responsesGenerator.spec.ts @@ -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 () => { const responses = await emitResponsesFromTypeSpec(` @get op read(): {@header contentType: "text/plain", @body body: bytes}; `); @@ -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; } `