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
7 changes: 7 additions & 0 deletions .chronus/changes/http-client-java-response-replacement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-java"
---

Honor void and raw-byte response replacements in generated Java clients, preserving types used by typed response headers. Fix raw-byte conversion for text responses.
70 changes: 63 additions & 7 deletions packages/http-client-java/emitter/src/code-model-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,32 @@ export class CodeModelBuilder {
lroMetadata = this.processLroMetadata(codeModelOperation, sdkMethod);
}

// TCGC only supports response replacement with void or bytes.
// Infer replacement when no declared HTTP body type matches the method return type.
// A no-content variant alone must not classify a parameter-only override as a replacement.
let responseReplacement: "void" | "bytes" | undefined =
sdkMethod.isOverride &&
Comment thread
weidongxu-microsoft marked this conversation as resolved.
sdkMethod.kind === "basic" &&
(sdkMethod.response.type === undefined || sdkMethod.response.type.kind === "bytes") &&
httpOperation.responses.some((response) => response.type !== sdkMethod.response.type) &&
!httpOperation.responses.some(
(response) => response.type !== undefined && response.type === sdkMethod.response.type,
)
Comment thread
weidongxu-microsoft marked this conversation as resolved.
? sdkMethod.response.type === undefined
? "void"
: "bytes"
: undefined;
if (
responseReplacement === "bytes" &&
httpOperation.responses.every((response) => response.type === undefined)
) {
reportDiagnostic(this.program, {
code: "bytes-response-replacement-without-body",
format: { operationName },
target: sdkMethod.__raw ?? httpOperation.__raw.operation,
});
responseReplacement = undefined;
}
// responses
for (const response of sdkMethod.operation.responses) {
this.processResponse(
Expand All @@ -1199,6 +1225,7 @@ export class CodeModelBuilder {
response,
lroMetadata.longRunning,
false,
responseReplacement,
);
}

Expand Down Expand Up @@ -2381,12 +2408,39 @@ export class CodeModelBuilder {
sdkResponse: SdkHttpResponse | SdkHttpErrorResponse,
longRunning: boolean,
isErrorResponse: boolean,
responseReplacement?: "void" | "bytes",
) {
// TODO: what to do if more than 1 response?
// It happens when the response type is Union, on one status code.
// let response: Response;
const headers: Array<HttpHeader> = [];

// Azure V1 replaces only the convenience return type, preserving the protocol response.
// Azure V2/ClientCore replace the operation response so the proxy handles void/raw bytes.
const replaceConvenienceResponse =
!isErrorResponse && responseReplacement !== undefined && this.isAzureV1();
const replaceResponseBodyType =
!isErrorResponse && responseReplacement !== undefined && !this.isAzureV1();
let bodyType: SdkType | undefined = sdkResponse.type;
if (!isErrorResponse && responseReplacement !== undefined) {
switch (responseReplacement) {
case "void":
if (replaceConvenienceResponse && op.convenienceApi) {
op.convenienceApi.response = createResponse();
} else if (replaceResponseBodyType) {
bodyType = undefined;
}
break;
case "bytes":
if (replaceConvenienceResponse && op.convenienceApi) {
op.convenienceApi.response = createBinaryResponse();
}
Comment thread
weidongxu-microsoft marked this conversation as resolved.
Comment thread
weidongxu-microsoft marked this conversation as resolved.
break;
default:
fail(`Unsupported response replacement: ${responseReplacement satisfies never}`);
}
}

if (sdkResponse.headers) {
for (const header of sdkResponse.headers) {
const schema = this.processSchema(header.type, header.name);
Expand Down Expand Up @@ -2423,10 +2477,9 @@ export class CodeModelBuilder {
}
}

const bodyType: SdkType | undefined = sdkResponse.type;
let trackConvenienceApi: boolean = Boolean(op.convenienceApi);
let trackResponseBody: boolean = Boolean(op.convenienceApi) && !replaceConvenienceResponse;
const trackResponseHeadersAsModel =
trackConvenienceApi &&
Boolean(op.convenienceApi) &&
(op.convenienceApi?.responseHeadersAsModel === true ||
(this.isAzureV1() && this.options["max-overload"] === "model"));

Expand All @@ -2441,7 +2494,10 @@ export class CodeModelBuilder {
}

let responseBodyIsFile: boolean = false;
if (
if (replaceResponseBodyType && responseReplacement === "bytes" && bodyType) {
// Raw bytes bypass JSON/base64 decoding even for application/json responses.
responseBodyIsFile = true;
} else if (
bodyType &&
bodyType.kind === "model" &&
bodyType.serializationOptions.binary &&
Expand Down Expand Up @@ -2481,7 +2537,7 @@ export class CodeModelBuilder {
if (longRunning) {
// LRO uses the LroMetadata for poll/final result, not the response of activation request
// hence the schema below is not tracked for convenience API
trackConvenienceApi = false;
trackResponseBody = false;
}

// schema (usually JSON)
Expand Down Expand Up @@ -2544,7 +2600,7 @@ export class CodeModelBuilder {
if (isSchemaResponse(response)) {
this.trackSchemaUsage(response.schema, { usage: [SchemaContext.Exception] });

if (trackConvenienceApi) {
if (trackResponseBody) {
let outputErrorModel = false;
if (!this.isBranded()) {
outputErrorModel = true;
Expand Down Expand Up @@ -2572,7 +2628,7 @@ export class CodeModelBuilder {
if (isSchemaResponse(response)) {
this.trackSchemaUsage(response.schema, { usage: [SchemaContext.Output] });

if (trackConvenienceApi) {
if (trackResponseBody) {
this.trackSchemaUsage(response.schema, {
usage: [op.internalApi ? SchemaContext.Internal : SchemaContext.Public],
});
Expand Down
3 changes: 3 additions & 0 deletions packages/http-client-java/emitter/src/common/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export interface Operation extends Aspect {
export interface ConvenienceApi extends Metadata {
requests?: Array<Request>;

/** Overrides the convenience return type without changing the protocol response. */
response?: Response;

/**
* Whether the convenience method returns the significant response headers as a strongly-typed model
* (opt-in via the "responseHeadersAsModel" client option). Only applicable to data-plane operations
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This warning is issued when `replaceResponseWithBytes` targets an operation with no declared HTTP response body.

## Impact

The emitter cannot fabricate response bytes. It ignores the response type override and preserves the original response type in all Java flavors.

## How to Fix

Remove the bytes replacement, or declare the HTTP response body if the service actually returns one.

## Suppression

Suppressing this warning does not enable the replacement; the original response type is still used.
7 changes: 7 additions & 0 deletions packages/http-client-java/emitter/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ export const $lib = createTypeSpecLibrary({
"Spread JSON merge-patch payload is not supported. The reason is that a property in JSON merge-patch payload class can: set a value; not set so that value does not change; set to null to remove the value. A parameter on method cannot distinguish the latter 2 cases.",
},
},
"bytes-response-replacement-without-body": {
...doc("bytes-response-replacement-without-body"),
severity: "warning",
messages: {
default: paramMessage`Cannot replace the response of operation '${"operationName"}' with bytes: the emitter cannot fabricate bytes when the HTTP response has no body. The response type override is ignored.`,
},
},
"type-not-supported-on-text-plain": {
...doc("type-not-supported-on-text-plain"),
severity: "warning",
Expand Down
Loading
Loading