Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
56 changes: 55 additions & 1 deletion packages/typespec-client-generator-core/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ import { getResponseAsBool, shouldOmitSlashFromEmptyRoute } from "./decorators.j
import {
CollectionFormat,
SdkBodyParameter,
SdkBuiltInType,
SdkClientType,
SdkCookieParameter,
SdkEnumType,
SdkHeaderParameter,
SdkHttpErrorResponse,
SdkHttpOperation,
Expand All @@ -51,6 +53,7 @@ import {
SdkStreamMetadata,
SdkType,
TCGCContext,
UsageFlags,
} from "./interfaces.js";
import {
compareModelProperties,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]);
}
Expand Down
111 changes: 111 additions & 0 deletions packages/typespec-client-generator-core/test/methods/file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Loading