From 259f7913340fc453e3a710c5c6ffe3e6ac449e4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 02:55:32 +0000 Subject: [PATCH 1/9] Initial plan From d0f213fcba01dbcba7f3455270435f4f1e5c3adb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 03:10:32 +0000 Subject: [PATCH 2/9] Fix File support inconsistencies: constant contentType for uploads, serializedName for response headers, enum accept for multiple content types Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../src/http.ts | 51 ++++++++++-- .../test/methods/file.test.ts | 82 +++++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index b8319088c5..fef3c07c23 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 } from "./decorators.js"; import { CollectionFormat, SdkBodyParameter, + SdkBuiltInType, SdkClientType, SdkCookieParameter, + SdkEnumType, SdkHeaderParameter, SdkHttpErrorResponse, SdkHttpOperation, @@ -51,6 +53,7 @@ import { SdkStreamMetadata, SdkType, TCGCContext, + UsageFlags, } from "./interfaces.js"; import { compareModelProperties, @@ -366,34 +369,70 @@ function createContentTypeOrAcceptHeader( bodyObject: SdkBodyParameter | SdkHttpResponse | SdkHttpErrorResponse, ): Omit { const name = bodyObject.kind === "body" ? "contentType" : "accept"; - let type: SdkType = getTypeSpecBuiltInType(context, "string"); + const stringType: SdkBuiltInType = getTypeSpecBuiltInType(context, "string"); + let type: SdkType = stringType; // for contentType, we treat it as a constant IFF there's one value and it's one of: // - application/json // - text/plain // - application/octet-stream + // - body is a File type (the content type is constrained by the File type itself) // this is to prevent a breaking change when a service adds more content types in the future. // e.g. the service accepting image/png then later image/jpeg should _not_ be a breaking change. // // for accept, we treat it as a constant IFF there's a single value. adding more content types // for this case is considered a breaking change for SDKs so we want to surface it as such. // e.g. the service returns image/png then later provides the option to return image/jpeg. + // when there are multiple accept content types, we create an enum to represent the valid values. + const isFileBody = + name === "contentType" && httpOperation.parameters.body?.bodyKind === "file"; if ( bodyObject.contentTypes && bodyObject.contentTypes.length === 1 && (isMediaTypeJson(bodyObject.contentTypes[0]) || isMediaTypeTextPlain(bodyObject.contentTypes[0]) || isMediaTypeOctetStream(bodyObject.contentTypes[0]) || - name === "accept") + name === "accept" || + isFileBody) ) { - // in this case, we just want a content type of application/json type = { kind: "constant", value: bodyObject.contentTypes[0], - valueType: type, + valueType: stringType, name: `${httpOperation.operation.name}ContentType`, isGeneratedName: true, decorators: [], }; + } else if ( + bodyObject.contentTypes && + bodyObject.contentTypes.length > 1 && + name === "accept" + ) { + 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 @@ -575,7 +614,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..ccd0a725df 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,85 @@ 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 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"); +}); From d3033a538a2818b3ca4c16e7e4cd130820e00bb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 03:15:10 +0000 Subject: [PATCH 3/9] Add chronus changeset for File support inconsistency fixes Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../fix-file-support-inconsistency-2026-03-11-03-12-00.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/fix-file-support-inconsistency-2026-03-11-03-12-00.md 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..060ef10b60 --- /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 support inconsistencies: contentType is now constant for File uploads with specific content types, response contentType header serializedName is set correctly, and accept header uses enum for multiple content types From 83414bc96fae23524b7e1df8c04b59cb4f58b924 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:34:14 +0000 Subject: [PATCH 4/9] Add test for File download with single content type verifying constant accept header Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/510fb20a-47fa-46c9-b0a3-ed52091ece89 --- .../test/methods/file.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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 ccd0a725df..4f77ac774e 100644 --- a/packages/typespec-client-generator-core/test/methods/file.test.ts +++ b/packages/typespec-client-generator-core/test/methods/file.test.ts @@ -319,6 +319,35 @@ it("file download with json content type should have correct contentType respons 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( ` From 54db0310a602ee7bec11c7a54796c30924553ac9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:57:21 +0000 Subject: [PATCH 5/9] Simplify contentType/accept logic: always use constant for single content type Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/99a377b1-bd10-4d4d-9a9f-6f047bf02159 --- .../src/http.ts | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 09c92d9e4d..3c74a7dcfe 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -72,7 +72,6 @@ import { isSubscriptionId, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; -import { isMediaTypeJson, isMediaTypeOctetStream, isMediaTypeTextPlain } from "./media-types.js"; import { getCrossLanguageDefinitionId, getEffectivePayloadType, @@ -378,29 +377,9 @@ function createContentTypeOrAcceptHeader( const name = bodyObject.kind === "body" ? "contentType" : "accept"; const stringType: SdkBuiltInType = getTypeSpecBuiltInType(context, "string"); let type: SdkType = stringType; - // for contentType, we treat it as a constant IFF there's one value and it's one of: - // - application/json - // - text/plain - // - application/octet-stream - // - body is a File type (the content type is constrained by the File type itself) - // this is to prevent a breaking change when a service adds more content types in the future. - // e.g. the service accepting image/png then later image/jpeg should _not_ be a breaking change. - // - // for accept, we treat it as a constant IFF there's a single value. adding more content types - // for this case is considered a breaking change for SDKs so we want to surface it as such. - // e.g. the service returns image/png then later provides the option to return image/jpeg. - // when there are multiple accept content types, we create an enum to represent the valid values. - const isFileBody = - name === "contentType" && httpOperation.parameters.body?.bodyKind === "file"; - if ( - bodyObject.contentTypes && - bodyObject.contentTypes.length === 1 && - (isMediaTypeJson(bodyObject.contentTypes[0]) || - isMediaTypeTextPlain(bodyObject.contentTypes[0]) || - isMediaTypeOctetStream(bodyObject.contentTypes[0]) || - name === "accept" || - isFileBody) - ) { + // If there's only a single content type, we treat it as a constant. + // If there are multiple content types, we create an enum to represent the valid values. + if (bodyObject.contentTypes && bodyObject.contentTypes.length === 1) { type = { kind: "constant", value: bodyObject.contentTypes[0], From 7c5f54940938fe628de9c32fb36da11044df06d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:28:33 +0000 Subject: [PATCH 6/9] Revert simplification commit - restore media type restriction logic Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/725ace9d-fc57-4a06-a779-127f17121c65 --- .../src/http.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 3c74a7dcfe..09c92d9e4d 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -72,6 +72,7 @@ import { isSubscriptionId, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; +import { isMediaTypeJson, isMediaTypeOctetStream, isMediaTypeTextPlain } from "./media-types.js"; import { getCrossLanguageDefinitionId, getEffectivePayloadType, @@ -377,9 +378,29 @@ function createContentTypeOrAcceptHeader( const name = bodyObject.kind === "body" ? "contentType" : "accept"; const stringType: SdkBuiltInType = getTypeSpecBuiltInType(context, "string"); let type: SdkType = stringType; - // If there's only a single content type, we treat it as a constant. - // If there are multiple content types, we create an enum to represent the valid values. - if (bodyObject.contentTypes && bodyObject.contentTypes.length === 1) { + // for contentType, we treat it as a constant IFF there's one value and it's one of: + // - application/json + // - text/plain + // - application/octet-stream + // - body is a File type (the content type is constrained by the File type itself) + // this is to prevent a breaking change when a service adds more content types in the future. + // e.g. the service accepting image/png then later image/jpeg should _not_ be a breaking change. + // + // for accept, we treat it as a constant IFF there's a single value. adding more content types + // for this case is considered a breaking change for SDKs so we want to surface it as such. + // e.g. the service returns image/png then later provides the option to return image/jpeg. + // when there are multiple accept content types, we create an enum to represent the valid values. + const isFileBody = + name === "contentType" && httpOperation.parameters.body?.bodyKind === "file"; + if ( + bodyObject.contentTypes && + bodyObject.contentTypes.length === 1 && + (isMediaTypeJson(bodyObject.contentTypes[0]) || + isMediaTypeTextPlain(bodyObject.contentTypes[0]) || + isMediaTypeOctetStream(bodyObject.contentTypes[0]) || + name === "accept" || + isFileBody) + ) { type = { kind: "constant", value: bodyObject.contentTypes[0], From 301d91171007840fda1df08a4f1bb616aa9129a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:32:51 +0000 Subject: [PATCH 7/9] Apply media type conditions to accept too: check json/text/octet-stream/file for both contentType and accept Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/725ace9d-fc57-4a06-a779-127f17121c65 --- packages/typespec-client-generator-core/src/http.ts | 12 ++++++------ .../test/methods/parameters.test.ts | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 09c92d9e4d..4b64754fc9 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -378,7 +378,7 @@ function createContentTypeOrAcceptHeader( const name = bodyObject.kind === "body" ? "contentType" : "accept"; const stringType: SdkBuiltInType = getTypeSpecBuiltInType(context, "string"); let type: SdkType = stringType; - // for contentType, we treat it as a constant IFF there's one value and it's one of: + // We treat a single content type as a constant IFF it's one of: // - application/json // - text/plain // - application/octet-stream @@ -386,19 +386,19 @@ function createContentTypeOrAcceptHeader( // this is to prevent a breaking change when a service adds more content types in the future. // e.g. the service accepting image/png then later image/jpeg should _not_ be a breaking change. // - // for accept, we treat it as a constant IFF there's a single value. adding more content types - // for this case is considered a breaking change for SDKs so we want to surface it as such. - // e.g. the service returns image/png then later provides the option to return image/jpeg. // when there are multiple accept content types, we create an enum to represent the valid values. const isFileBody = - name === "contentType" && httpOperation.parameters.body?.bodyKind === "file"; + name === "contentType" + ? httpOperation.parameters.body?.bodyKind === "file" + : httpOperation.responses.some((r) => + r.responses.some((rc) => rc.body?.bodyKind === "file"), + ); if ( bodyObject.contentTypes && bodyObject.contentTypes.length === 1 && (isMediaTypeJson(bodyObject.contentTypes[0]) || isMediaTypeTextPlain(bodyObject.contentTypes[0]) || isMediaTypeOctetStream(bodyObject.contentTypes[0]) || - name === "accept" || isFileBody) ) { type = { diff --git a/packages/typespec-client-generator-core/test/methods/parameters.test.ts b/packages/typespec-client-generator-core/test/methods/parameters.test.ts index 46971fc046..80e6821377 100644 --- a/packages/typespec-client-generator-core/test/methods/parameters.test.ts +++ b/packages/typespec-client-generator-core/test/methods/parameters.test.ts @@ -890,7 +890,7 @@ it("ensure accept is a constant if only one possibility (json)", async () => { strictEqual(method.response.type, sdkPackage.models[0]); }); -it("ensure accept is a constant if only one possibility (non-json)", async () => { +it("ensure accept is string for non-standard content type (non-file body)", async () => { const { program } = await SimpleTesterWithService.compile(` @get op default(): { @header @@ -914,9 +914,7 @@ it("ensure accept is a constant if only one possibility (non-json)", async () => strictEqual(serviceContentTypeParam.name, "accept"); strictEqual(serviceContentTypeParam.serializedName, "Accept"); strictEqual(serviceContentTypeParam.clientDefaultValue, undefined); - strictEqual(serviceContentTypeParam.type.kind, "constant"); - strictEqual(serviceContentTypeParam.type.value, "image/png"); - strictEqual(serviceContentTypeParam.type.valueType.kind, "string"); + strictEqual(serviceContentTypeParam.type.kind, "string"); strictEqual(serviceOperation.responses.length, 1); const response = serviceOperation.responses.find((x) => x.statusCodes === 200); From 75fc75feb54c000fdbb0f4492d6776fb7387f854 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:59:09 +0000 Subject: [PATCH 8/9] Restore original createContentTypeOrAcceptHeader logic, add separate File type branch for constant/enum Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/e4a8a52d-745c-4d6d-9e9d-e47477ad5e1f --- .../src/http.ts | 101 ++++++++++-------- .../test/methods/parameters.test.ts | 6 +- 2 files changed, 61 insertions(+), 46 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 4b64754fc9..2e8cd9ffe7 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -376,70 +376,83 @@ function createContentTypeOrAcceptHeader( bodyObject: SdkBodyParameter | SdkHttpResponse | SdkHttpErrorResponse, ): Omit { const name = bodyObject.kind === "body" ? "contentType" : "accept"; - const stringType: SdkBuiltInType = getTypeSpecBuiltInType(context, "string"); - let type: SdkType = stringType; - // We treat a single content type as a constant IFF it's one of: + let type: SdkType = getTypeSpecBuiltInType(context, "string"); + // for contentType, we treat it as a constant IFF there's one value and it's one of: // - application/json // - text/plain // - application/octet-stream - // - body is a File type (the content type is constrained by the File type itself) // this is to prevent a breaking change when a service adds more content types in the future. // e.g. the service accepting image/png then later image/jpeg should _not_ be a breaking change. // - // when there are multiple accept content types, we create an enum to represent the valid values. - const isFileBody = - name === "contentType" - ? httpOperation.parameters.body?.bodyKind === "file" - : httpOperation.responses.some((r) => - r.responses.some((rc) => rc.body?.bodyKind === "file"), - ); + // for accept, we treat it as a constant IFF there's a single value. adding more content types + // for this case is considered a breaking change for SDKs so we want to surface it as such. + // e.g. the service returns image/png then later provides the option to return image/jpeg. if ( bodyObject.contentTypes && bodyObject.contentTypes.length === 1 && (isMediaTypeJson(bodyObject.contentTypes[0]) || isMediaTypeTextPlain(bodyObject.contentTypes[0]) || isMediaTypeOctetStream(bodyObject.contentTypes[0]) || - isFileBody) + name === "accept") ) { + // in this case, we just want a content type of application/json type = { kind: "constant", value: bodyObject.contentTypes[0], - valueType: stringType, + valueType: type, name: `${httpOperation.operation.name}ContentType`, isGeneratedName: true, decorators: [], }; - } else if ( - bodyObject.contentTypes && - bodyObject.contentTypes.length > 1 && - name === "accept" - ) { - 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; + } 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 diff --git a/packages/typespec-client-generator-core/test/methods/parameters.test.ts b/packages/typespec-client-generator-core/test/methods/parameters.test.ts index 80e6821377..46971fc046 100644 --- a/packages/typespec-client-generator-core/test/methods/parameters.test.ts +++ b/packages/typespec-client-generator-core/test/methods/parameters.test.ts @@ -890,7 +890,7 @@ it("ensure accept is a constant if only one possibility (json)", async () => { strictEqual(method.response.type, sdkPackage.models[0]); }); -it("ensure accept is string for non-standard content type (non-file body)", async () => { +it("ensure accept is a constant if only one possibility (non-json)", async () => { const { program } = await SimpleTesterWithService.compile(` @get op default(): { @header @@ -914,7 +914,9 @@ it("ensure accept is string for non-standard content type (non-file body)", asyn strictEqual(serviceContentTypeParam.name, "accept"); strictEqual(serviceContentTypeParam.serializedName, "Accept"); strictEqual(serviceContentTypeParam.clientDefaultValue, undefined); - strictEqual(serviceContentTypeParam.type.kind, "string"); + strictEqual(serviceContentTypeParam.type.kind, "constant"); + strictEqual(serviceContentTypeParam.type.value, "image/png"); + strictEqual(serviceContentTypeParam.type.valueType.kind, "string"); strictEqual(serviceOperation.responses.length, 1); const response = serviceOperation.responses.find((x) => x.statusCodes === 200); From 0028cda75d3512e1b734d7bc2950eeafc67a72f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:07:33 +0000 Subject: [PATCH 9/9] Refine changeset description to match latest code changes Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/8f9b3000-67de-4a91-a639-02c1c80eb033 --- .../fix-file-support-inconsistency-2026-03-11-03-12-00.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 060ef10b60..aa86df36a2 100644 --- 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 @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-client-generator-core" --- -Fix File support inconsistencies: contentType is now constant for File uploads with specific content types, response contentType header serializedName is set correctly, and accept header uses enum for multiple content types +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