diff --git a/.chronus/changes/fix-file-support-inconsistency-2026-03-11-03-12-00.md b/.chronus/changes/fix-file-support-inconsistency-2026-03-11-03-12-00.md new file mode 100644 index 0000000000..aa86df36a2 --- /dev/null +++ b/.chronus/changes/fix-file-support-inconsistency-2026-03-11-03-12-00.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Fix File type contentType/accept header handling: add a new branch in `createContentTypeOrAcceptHeader` for File type bodies to produce constant (single content type) or enum (multiple content types) for both contentType and accept params, and fix response contentType header serializedName fallback to "Content-Type" when `@header` is missing diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 782337a87d..2e8cd9ffe7 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -35,8 +35,10 @@ import { getResponseAsBool, shouldOmitSlashFromEmptyRoute } from "./decorators.j import { CollectionFormat, SdkBodyParameter, + SdkBuiltInType, SdkClientType, SdkCookieParameter, + SdkEnumType, SdkHeaderParameter, SdkHttpErrorResponse, SdkHttpOperation, @@ -51,6 +53,7 @@ import { SdkStreamMetadata, SdkType, TCGCContext, + UsageFlags, } from "./interfaces.js"; import { compareModelProperties, @@ -401,6 +404,55 @@ function createContentTypeOrAcceptHeader( isGeneratedName: true, decorators: [], }; + } else if (bodyObject.contentTypes) { + // For File type bodies, the content type is constrained by the File type itself. + // Follow the content type to add a constant (single) or enum (multiple) param. + const isFileBody = + name === "contentType" + ? httpOperation.parameters.body?.bodyKind === "file" + : httpOperation.responses.some((r) => + r.responses.some((rc) => rc.body?.bodyKind === "file"), + ); + if (isFileBody) { + const stringType: SdkBuiltInType = getTypeSpecBuiltInType(context, "string"); + if (bodyObject.contentTypes.length === 1) { + type = { + kind: "constant", + value: bodyObject.contentTypes[0], + valueType: stringType, + name: `${httpOperation.operation.name}ContentType`, + isGeneratedName: true, + decorators: [], + }; + } else if (bodyObject.contentTypes.length > 1) { + const enumType: SdkEnumType = { + kind: "enum", + name: `${httpOperation.operation.name}ContentType`, + isGeneratedName: true, + namespace: "", + valueType: stringType, + values: [], + isFixed: true, + isFlags: false, + usage: UsageFlags.None, + access: "public", + crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, httpOperation.operation)}.${name}`, + apiVersions: bodyObject.apiVersions, + isUnionAsEnum: false, + decorators: [], + }; + enumType.values = bodyObject.contentTypes.map((ct) => ({ + kind: "enumvalue" as const, + name: ct, + value: ct, + enumType, + valueType: stringType, + crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, httpOperation.operation)}.${name}.${ct}`, + decorators: [], + })); + type = enumType; + } + } } const optional = bodyObject.kind === "body" ? bodyObject.optional : false; // No need for clientDefaultValue because it's a constant, it only has one value @@ -582,7 +634,9 @@ function getSdkHttpResponseAndExceptions( ), __raw: header, kind: "responseheader", - serializedName: getHeaderFieldName(context.program, header), + serializedName: + getHeaderFieldName(context.program, header) ?? + (header === innerResponse.body?.contentTypeProperty ? "Content-Type" : header.name), }); context.__responseHeaderCache.set(header, headers[headers.length - 1]); } diff --git a/packages/typespec-client-generator-core/test/methods/file.test.ts b/packages/typespec-client-generator-core/test/methods/file.test.ts index 144d53a487..4f77ac774e 100644 --- a/packages/typespec-client-generator-core/test/methods/file.test.ts +++ b/packages/typespec-client-generator-core/test/methods/file.test.ts @@ -266,3 +266,114 @@ it("file type headers should have correct serializedName", async () => { // The serializedName should be "Content-Type", not "contentType" strictEqual(contentTypeParam.serializedName, "Content-Type"); }); + +it("file upload with specific content type should have constant contentType", async () => { + const { program } = await SimpleTester.compile( + ` + @service + namespace TestService { + op uploadFileSpecificContentType(@body file: File<"image/png">): void; + } + `, + ); + const context = await createSdkContextForTester(program); + const sdkPackage = context.sdkPackage; + const method = sdkPackage.clients[0].methods[0]; + strictEqual(method.name, "uploadFileSpecificContentType"); + // The contentType method parameter should be constant, not string + const contentTypeMethodParam = method.parameters.find((p) => p.name === "contentType"); + ok(contentTypeMethodParam); + strictEqual(contentTypeMethodParam.type.kind, "constant"); + strictEqual(contentTypeMethodParam.type.value, "image/png"); + // The Content-Type header should also be constant + const httpOperation = method.operation; + const contentTypeHeader = httpOperation.parameters.find( + (p) => p.kind === "header" && p.name === "contentType", + ); + ok(contentTypeHeader); + strictEqual(contentTypeHeader.type.kind, "constant"); + strictEqual(contentTypeHeader.type.value, "image/png"); + strictEqual(contentTypeHeader.serializedName, "Content-Type"); +}); + +it("file download with json content type should have correct contentType response header serializedName", async () => { + const { program } = await SimpleTester.compile( + ` + @service + namespace TestService { + op downloadFileJsonContentType(): File<"application/json", string>; + } + `, + ); + const context = await createSdkContextForTester(program); + const sdkPackage = context.sdkPackage; + const method = sdkPackage.clients[0].methods[0]; + strictEqual(method.name, "downloadFileJsonContentType"); + const httpOperation = method.operation; + const response = httpOperation.responses[0]; + ok(response); + ok(response.type); + // Check that response contentType header has proper serializedName + const contentTypeHeader = response.headers.find((h) => h.name === "contentType"); + ok(contentTypeHeader); + strictEqual(contentTypeHeader.serializedName, "Content-Type"); +}); + +it("file download with single content type should have constant accept header", async () => { + const { program } = await SimpleTester.compile( + ` + @service + namespace TestService { + op downloadFileSingleContentType(): File<"image/png">; + } + `, + ); + const context = await createSdkContextForTester(program); + const sdkPackage = context.sdkPackage; + const method = sdkPackage.clients[0].methods[0]; + strictEqual(method.name, "downloadFileSingleContentType"); + // The accept method parameter should be constant, not string + const acceptMethodParam = method.parameters.find((p) => p.name === "accept"); + ok(acceptMethodParam); + strictEqual(acceptMethodParam.type.kind, "constant"); + strictEqual(acceptMethodParam.type.value, "image/png"); + // The Accept header should also be constant + const httpOperation = method.operation; + const acceptHeader = httpOperation.parameters.find( + (p) => p.kind === "header" && p.name === "accept", + ); + ok(acceptHeader); + strictEqual(acceptHeader.type.kind, "constant"); + strictEqual(acceptHeader.type.value, "image/png"); + strictEqual(acceptHeader.serializedName, "Accept"); +}); + +it("file download with multiple content types should have enum accept header", async () => { + const { program } = await SimpleTester.compile( + ` + @service + namespace TestService { + op downloadFileMultipleContentTypes(): File<"image/png" | "image/jpeg">; + } + `, + ); + const context = await createSdkContextForTester(program); + const sdkPackage = context.sdkPackage; + const method = sdkPackage.clients[0].methods[0]; + strictEqual(method.name, "downloadFileMultipleContentTypes"); + // The accept method parameter should be an enum, not string + const acceptMethodParam = method.parameters.find((p) => p.name === "accept"); + ok(acceptMethodParam); + strictEqual(acceptMethodParam.type.kind, "enum"); + strictEqual(acceptMethodParam.type.values.length, 2); + ok(acceptMethodParam.type.values.find((v) => v.value === "image/png")); + ok(acceptMethodParam.type.values.find((v) => v.value === "image/jpeg")); + // The Accept header should also be an enum + const httpOperation = method.operation; + const acceptHeader = httpOperation.parameters.find( + (p) => p.kind === "header" && p.name === "accept", + ); + ok(acceptHeader); + strictEqual(acceptHeader.type.kind, "enum"); + strictEqual(acceptHeader.serializedName, "Accept"); +});