From dad5d0fc6a1e54102c723cdb31ca6328e9dd647a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Tue, 14 Jul 2026 09:40:34 +0900 Subject: [PATCH 1/5] Honor InputStream ownership in Java native clients Java native generation previously kept File-oriented response handling and Jackson request serialization even when file and binary types were explicitly mapped to java.io.InputStream. Mark only resolved native operations, publish mapped request streams directly, and transfer returned response-stream ownership past existing finally cleanup. Constraint: Streaming behavior must be driven by the resolved java.io.InputStream mapping and operation.bodyParam, not isFile/isBinary flags or a new option. Constraint: Existing native File downloads, JSON serialization, gzip wrapping, and finally cleanup must remain intact. Rejected: Infer streaming from isFile/isBinary | would change default File behavior independently of configured mappings. Confidence: high Scope-risk: narrow Directive: Keep response ownership transfer paired with nulling localVarResponseBody before the existing finally block. Tested: JavaClientCodegenTest issue24057 sync/async/control cases plus native empty-response sync/async neighbors. Tested: Generated sync and async native Maven clients packaged with file/binary=InputStream and InputStream=java.io.InputStream mappings. Not-tested: Full repository test suite. --- .../codegen/languages/JavaClientCodegen.java | 13 ++++ .../Java/libraries/native/api.mustache | 28 ++++++- .../codegen/java/JavaClientCodegenTest.java | 73 +++++++++++++++++++ .../resources/3_0/java/native/issue24057.yaml | 48 ++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 8601c824fc88..557e38b51db1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -1011,6 +1011,14 @@ public int compare(CodegenParameter one, CodegenParameter another) { if ("String".equals(op.returnType) && op.producesTextPlain()) { op.vendorExtensions.put("x-java-text-plain-string", true); } + if (isLibrary(NATIVE)) { + if (mapsToInputStream(op.returnType)) { + op.vendorExtensions.put("x-java-native-response-input-stream", true); + } + if (op.bodyParam != null && mapsToInputStream(op.bodyParam.dataType)) { + op.vendorExtensions.put("x-java-native-body-input-stream", true); + } + } } } @@ -1045,6 +1053,11 @@ public int compare(CodegenParameter one, CodegenParameter another) { return objs; } + private boolean mapsToInputStream(String dataType) { + return "java.io.InputStream".equals(dataType) + || "java.io.InputStream".equals(importMapping.get(dataType)); + } + @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache index bc9a8833c12d..0b9cb7da9108 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache @@ -492,6 +492,11 @@ public class {{classname}} { ); } + {{#vendorExtensions.x-java-native-response-input-stream}} + {{{returnType}}} responseValue = localVarResponseBody; + localVarResponseBody = null; + {{/vendorExtensions.x-java-native-response-input-stream}} + {{^vendorExtensions.x-java-native-response-input-stream}} {{^isResponseFile}}{{#isResponseBinary}} Byte[] responseValue = localVarResponseBody.readAllBytes(); {{/isResponseBinary}}{{/isResponseFile}} @@ -503,6 +508,7 @@ public class {{classname}} { String responseBody = new String(localVarResponseBody.readAllBytes()); {{{returnType}}} responseValue = responseBody.isBlank()? null: memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {}); {{/isResponseFile}}{{/isResponseBinary}} + {{/vendorExtensions.x-java-native-response-input-stream}} return new ApiResponse<{{{returnType}}}>( localVarResponse.statusCode(), @@ -582,6 +588,11 @@ public class {{classname}} { ) ); } + {{#vendorExtensions.x-java-native-response-input-stream}} + {{{returnType}}} responseValue = localVarResponseBody; + localVarResponseBody = null; + {{/vendorExtensions.x-java-native-response-input-stream}} + {{^vendorExtensions.x-java-native-response-input-stream}} {{^isResponseFile}}{{#isResponseBinary}} Byte[] responseValue = localVarResponseBody.readAllBytes(); {{/isResponseBinary}}{{/isResponseFile}} @@ -592,6 +603,7 @@ public class {{classname}} { String responseBody = new String(localVarResponseBody.readAllBytes()); {{{returnType}}} responseValue = responseBody.isBlank()? null: memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {}); {{/isResponseFile}}{{/isResponseBinary}} + {{/vendorExtensions.x-java-native-response-input-stream}} return CompletableFuture.completedFuture( new ApiResponse<{{{returnType}}}>( localVarResponse.statusCode(), @@ -723,6 +735,19 @@ public class {{classname}} { localVarRequestBuilder.header("Accept-Encoding", "gzip"); {{/useGzipFeature}} + {{#vendorExtensions.x-java-native-body-input-stream}} + {{#bodyParam}} + {{#useGzipFeature}} + Supplier localVarRequestBodySupplier = () -> {{paramName}}; + localVarRequestBuilder.header("Content-Encoding", "gzip"); + localVarRequestBuilder.method("{{httpMethod}}", ApiClient.gzipRequestBody(localVarRequestBodySupplier)); + {{/useGzipFeature}} + {{^useGzipFeature}} + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofInputStream(() -> {{paramName}})); + {{/useGzipFeature}} + {{/bodyParam}} + {{/vendorExtensions.x-java-native-body-input-stream}} + {{^vendorExtensions.x-java-native-body-input-stream}} {{#bodyParam}} {{#isString}} {{#useGzipFeature}} @@ -750,6 +775,7 @@ public class {{classname}} { } {{/isString}} {{/bodyParam}} + {{/vendorExtensions.x-java-native-body-input-stream}} {{^bodyParam}} {{#hasFormParams}} {{#isMultipart}} @@ -956,4 +982,4 @@ public class {{classname}} { {{/vendorExtensions.x-group-parameters}} {{/operation}} } -{{/operations}} \ No newline at end of file +{{/operations}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 68a68136759b..b39c12a59c70 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -3870,6 +3870,79 @@ public void callNativeServiceWithEmptyResponseSync() throws IOException { ); } + @Test + public void issue24057NativeInputStreamSync() { + Path apiFile = generateIssue24057Native(false, true); + + assertFileContains( + apiFile, + "public InputStream download() throws ApiException {", + "InputStream responseValue = localVarResponseBody;", + "localVarResponseBody = null;", + "HttpRequest.BodyPublishers.ofInputStream(() -> body)" + ); + assertFileNotContains( + apiFile, + "File responseValue = downloadFileFromResponse(localVarResponse, localVarResponseBody);", + "memberVarObjectMapper.writeValueAsBytes(body)" + ); + } + + @Test + public void issue24057NativeInputStreamAsync() { + Path apiFile = generateIssue24057Native(true, true); + + assertFileContains( + apiFile, + "public CompletableFuture download() throws ApiException {", + "InputStream responseValue = localVarResponseBody;", + "localVarResponseBody = null;", + "HttpRequest.BodyPublishers.ofInputStream(() -> body)" + ); + assertFileNotContains( + apiFile, + "File responseValue = downloadFileFromResponse(localVarResponse, localVarResponseBody);", + "memberVarObjectMapper.writeValueAsBytes(body)" + ); + } + + @Test + public void issue24057NativeDefaultMappingAndJsonControls() { + Path apiFile = generateIssue24057Native(false, false); + + assertFileContains( + apiFile, + "File responseValue = downloadFileFromResponse(localVarResponse, localVarResponseBody);", + "memberVarObjectMapper.writeValueAsBytes(payload)" + ); + } + + private Path generateIssue24057Native(boolean asyncNative, boolean mapStreams) { + Path output = newTempFolder(); + Map properties = new HashMap<>(); + properties.put(CodegenConstants.API_PACKAGE, "xyz.abcdef.api"); + properties.put("asyncNative", asyncNative); + properties.put(JavaClientCodegen.SUPPORT_STREAMING, true); + + CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName(JAVA_GENERATOR) + .setLibrary(JavaClientCodegen.NATIVE) + .setAdditionalProperties(properties) + .setInputSpec("src/test/resources/3_0/java/native/issue24057.yaml") + .setOutputDir(output.toString().replace("\\", "/")); + + if (mapStreams) { + configurator + .setTypeMappings(Map.of( + "file", "InputStream", + "binary", "InputStream")) + .setImportMappings(Map.of("InputStream", "java.io.InputStream")); + } + + new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + return output.resolve("src/main/java/xyz/abcdef/api/DefaultApi.java"); + } + @Test public void annotationLibraryDoesNotCauseImportConflicts() throws IOException { Map properties = new HashMap<>(); diff --git a/modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml b/modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml new file mode 100644 index 000000000000..3d403686267a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml @@ -0,0 +1,48 @@ +openapi: 3.0.3 +info: + title: Native InputStream mapping regression + version: 1.0.0 +paths: + /download: + get: + operationId: download + responses: + '200': + description: Binary response + content: + application/octet-stream: + schema: + type: string + format: binary + /upload: + post: + operationId: upload + requestBody: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '204': + description: Uploaded + /payload: + post: + operationId: createPayload + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Payload' + responses: + '204': + description: Created +components: + schemas: + Payload: + type: object + properties: + value: + type: string From d3d3d92405ea6c485f108f64e94f75c6d96e9352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Tue, 14 Jul 2026 09:52:03 +0900 Subject: [PATCH 2/5] Prevent mapped JSON from bypassing native serialization Native raw-stream ownership is valid only when the OpenAPI operation remains classified as binary or file and its resolved Java type is InputStream. Preserve normal Jackson request and response handling for non-binary schemas that users map to InputStream. Constraint: Raw stream paths require both original binary/file metadata and an exact java.io.InputStream resolution Constraint: The existing native template behavior remains unchanged Rejected: Gate only on resolved InputStream type | mapped JSON incorrectly bypasses serialization Confidence: high Scope-risk: narrow Reversibility: clean Directive: Do not remove the binary/file metadata guard without testing mapped JSON request and response schemas Tested: Genuine RED/GREEN regression; 6 focused issue and neighbor tests; fresh sync and async generated clients packaged with Maven Not-tested: Full repository suite and live HTTP integration --- .../codegen/languages/JavaClientCodegen.java | 6 ++- .../codegen/java/JavaClientCodegenTest.java | 45 ++++++++++++++++--- .../resources/3_0/java/native/issue24057.yaml | 8 +++- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 557e38b51db1..42c9ada80cd2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -1012,10 +1012,12 @@ public int compare(CodegenParameter one, CodegenParameter another) { op.vendorExtensions.put("x-java-text-plain-string", true); } if (isLibrary(NATIVE)) { - if (mapsToInputStream(op.returnType)) { + if ((op.isResponseBinary || op.isResponseFile) && mapsToInputStream(op.returnType)) { op.vendorExtensions.put("x-java-native-response-input-stream", true); } - if (op.bodyParam != null && mapsToInputStream(op.bodyParam.dataType)) { + if (op.bodyParam != null + && (op.bodyParam.isBinary || op.bodyParam.isFile) + && mapsToInputStream(op.bodyParam.dataType)) { op.vendorExtensions.put("x-java-native-body-input-stream", true); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index b39c12a59c70..2cf68b90a7a2 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -3917,7 +3917,38 @@ public void issue24057NativeDefaultMappingAndJsonControls() { ); } + @Test + public void issue24057NativeNonBinaryInputStreamMappingKeepsJsonHandling() { + Path apiFile = generateIssue24057Native( + false, + Collections.emptyMap(), + Map.of("Payload", "InputStream")); + + assertFileContains( + apiFile, + "byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(inputStream);", + "InputStream responseValue = responseBody.isBlank()? null: memberVarObjectMapper.readValue(responseBody, new TypeReference() {})" + ); + assertFileNotContains( + apiFile, + "InputStream responseValue = localVarResponseBody;", + "HttpRequest.BodyPublishers.ofInputStream(() -> inputStream)" + ); + } + private Path generateIssue24057Native(boolean asyncNative, boolean mapStreams) { + return generateIssue24057Native( + asyncNative, + mapStreams ? Map.of( + "file", "InputStream", + "binary", "InputStream") : Collections.emptyMap(), + Collections.emptyMap()); + } + + private Path generateIssue24057Native( + boolean asyncNative, + Map typeMappings, + Map schemaMappings) { Path output = newTempFolder(); Map properties = new HashMap<>(); properties.put(CodegenConstants.API_PACKAGE, "xyz.abcdef.api"); @@ -3931,12 +3962,14 @@ private Path generateIssue24057Native(boolean asyncNative, boolean mapStreams) { .setInputSpec("src/test/resources/3_0/java/native/issue24057.yaml") .setOutputDir(output.toString().replace("\\", "/")); - if (mapStreams) { - configurator - .setTypeMappings(Map.of( - "file", "InputStream", - "binary", "InputStream")) - .setImportMappings(Map.of("InputStream", "java.io.InputStream")); + if (!typeMappings.isEmpty()) { + configurator.setTypeMappings(typeMappings); + } + if (!schemaMappings.isEmpty()) { + configurator.setSchemaMappings(schemaMappings); + } + if (!typeMappings.isEmpty() || !schemaMappings.isEmpty()) { + configurator.setImportMappings(Map.of("InputStream", "java.io.InputStream")); } new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); diff --git a/modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml b/modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml index 3d403686267a..c92f313b3452 100644 --- a/modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/java/native/issue24057.yaml @@ -37,8 +37,12 @@ paths: schema: $ref: '#/components/schemas/Payload' responses: - '204': - description: Created + '200': + description: Created payload + content: + application/json: + schema: + $ref: '#/components/schemas/Payload' components: schemas: Payload: From 3a00805ac97bd05cb13f2a303a01d908ee8da382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Tue, 14 Jul 2026 10:54:09 +0900 Subject: [PATCH 3/5] Prevent stream ownership loss across native async races Native InputStream responses need an explicit delivery owner because CompletableFuture cancellation does not reliably stop an already-running dependent action. Bridge both async public surfaces, close results that lose delivery races, and propagate cancellation only to the HTTP transport so processing can perform cleanup. Request streams are atomically one-shot for plain and gzip publishers, and gzip cleanup is idempotent after EOF. Constraint: JDK BodyPublishers.ofInputStream may request a fresh stream for repeated subscriptions; the existing API accepts one caller-owned InputStream, not a reopenable supplier. Rejected: Reuse the same request InputStream on retry | a consumed or closed stream can silently publish empty data. Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve explicit async delivery bridges; cancelling the processing future can bypass undeliverable-stream cleanup. Tested: Focused JavaClientCodegenTest issue/neighbor scope 9/9; deterministic async ownership and plain/gzip replay runtime harness; gzip exact-close RED/GREEN; generated sync, async, and gzip Maven packages. Not-tested: Full repository test suite; live network redirect/retry behavior. --- .../Java/libraries/native/ApiClient.mustache | 9 +- .../Java/libraries/native/api.mustache | 76 +++- .../codegen/java/JavaClientCodegenTest.java | 136 +++++- .../resources/3_0/java/native/Generated.java | 25 ++ .../NativeInputStreamRuntimeHarness.java | 419 ++++++++++++++++++ 5 files changed, 653 insertions(+), 12 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/native/Generated.java create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/ApiClient.mustache index bbbccdeb5806..0d5cab6ce3fc 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/ApiClient.mustache @@ -538,7 +538,10 @@ public class ApiClient { */ public static HttpRequest.BodyPublisher gzipRequestBody(Supplier bodySupplier) { Objects.requireNonNull(bodySupplier, "bodySupplier must not be null"); - return HttpRequest.BodyPublishers.ofInputStream(() -> new GzipCompressingInputStream(bodySupplier)); + return HttpRequest.BodyPublishers.ofInputStream(() -> { + InputStream body = bodySupplier.get(); + return body == null ? null : new GzipCompressingInputStream(() -> body); + }); } private static final class GzipCompressingInputStream extends InputStream { @@ -565,15 +568,17 @@ public class ApiClient { private boolean fillBuffer() throws IOException { while (chunkPosition >= currentChunk.length) { buffer.reset(); - ensureInitialized(); if (finished) { return false; } + ensureInitialized(); int bytesRead = source.read(readBuffer); if (bytesRead == -1) { gzipStream.finish(); gzipStream.close(); + gzipStream = null; source.close(); + source = null; finished = true; } else { gzipStream.write(readBuffer, 0, bytesRead); diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache index 0b9cb7da9108..50bf854eec66 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache @@ -54,13 +54,12 @@ import java.util.Set; import java.util.function.UnaryOperator; {{/useUnaryInterceptor}} import java.util.function.Consumer; -{{#useGzipFeature}} import java.util.function.Supplier; -{{/useGzipFeature}} {{#asyncNative}} import java.util.concurrent.CompletableFuture; {{/asyncNative}} +import java.util.concurrent.atomic.AtomicReference; {{>generatedAnnotation}} @@ -384,8 +383,39 @@ public class {{classname}} { {{/asyncNative}} {{#asyncNative}} try { + {{#vendorExtensions.x-java-native-response-input-stream}} + CompletableFuture> localVarHttpInfoFuture = + {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); + CompletableFuture<{{{returnType}}}> localVarResult = new CompletableFuture<{{{returnType}}}>() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean localVarCancelled = super.cancel(mayInterruptIfRunning); + if (localVarCancelled) { + localVarHttpInfoFuture.cancel(mayInterruptIfRunning); + } + return localVarCancelled; + } + }; + localVarHttpInfoFuture.whenComplete((localVarApiResponse, localVarThrowable) -> { + if (localVarThrowable != null) { + localVarResult.completeExceptionally(localVarThrowable); + } else { + {{{returnType}}} localVarResponseBody = localVarApiResponse.getData(); + if (!localVarResult.complete(localVarResponseBody) && localVarResponseBody != null) { + try { + localVarResponseBody.close(); + } catch (IOException ignored) { + // The result is already terminal, so cleanup failure has no receiver. + } + } + } + }); + return localVarResult; + {{/vendorExtensions.x-java-native-response-input-stream}} + {{^vendorExtensions.x-java-native-response-input-stream}} return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers) .thenApply(ApiResponse::getData); + {{/vendorExtensions.x-java-native-response-input-stream}} } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -544,9 +574,17 @@ public class {{classname}} { {{#asyncNative}} try { HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); + {{#vendorExtensions.x-java-native-response-input-stream}} + CompletableFuture> localVarTransportFuture = memberVarHttpClient.sendAsync( + localVarRequestBuilder.build(), + HttpResponse.BodyHandlers.ofInputStream()); + CompletableFuture> localVarProcessingFuture = localVarTransportFuture.thenComposeAsync(localVarResponse -> { + {{/vendorExtensions.x-java-native-response-input-stream}} + {{^vendorExtensions.x-java-native-response-input-stream}} return memberVarHttpClient.sendAsync( localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream()).thenComposeAsync(localVarResponse -> { + {{/vendorExtensions.x-java-native-response-input-stream}} if (memberVarAsyncResponseInterceptor != null) { {{#useUnaryInterceptor}} localVarResponse = memberVarAsyncResponseInterceptor.apply(localVarResponse); @@ -631,6 +669,34 @@ public class {{classname}} { } } ); + {{#vendorExtensions.x-java-native-response-input-stream}} + CompletableFuture> localVarResult = + new CompletableFuture>() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean localVarCancelled = super.cancel(mayInterruptIfRunning); + if (localVarCancelled) { + localVarTransportFuture.cancel(mayInterruptIfRunning); + } + return localVarCancelled; + } + }; + localVarProcessingFuture.whenComplete((localVarApiResponse, localVarThrowable) -> { + if (localVarThrowable != null) { + localVarResult.completeExceptionally(localVarThrowable); + } else if (!localVarResult.complete(localVarApiResponse)) { + {{{returnType}}} localVarUndeliveredBody = localVarApiResponse.getData(); + if (localVarUndeliveredBody != null) { + try { + localVarUndeliveredBody.close(); + } catch (IOException ignored) { + // The result is already terminal, so cleanup failure has no receiver. + } + } + } + }); + return localVarResult; + {{/vendorExtensions.x-java-native-response-input-stream}} } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -737,13 +803,15 @@ public class {{classname}} { {{#vendorExtensions.x-java-native-body-input-stream}} {{#bodyParam}} + // Caller-provided InputStream bodies are one-shot; repeated sends fail deterministically. + AtomicReference localVarRequestBody = new AtomicReference<>({{paramName}}); + Supplier localVarRequestBodySupplier = () -> localVarRequestBody.getAndSet(null); {{#useGzipFeature}} - Supplier localVarRequestBodySupplier = () -> {{paramName}}; localVarRequestBuilder.header("Content-Encoding", "gzip"); localVarRequestBuilder.method("{{httpMethod}}", ApiClient.gzipRequestBody(localVarRequestBodySupplier)); {{/useGzipFeature}} {{^useGzipFeature}} - localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofInputStream(() -> {{paramName}})); + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofInputStream(localVarRequestBodySupplier)); {{/useGzipFeature}} {{/bodyParam}} {{/vendorExtensions.x-java-native-body-input-stream}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 2cf68b90a7a2..ca0e1b7633ce 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -50,8 +50,15 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; import java.io.File; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -3879,12 +3886,15 @@ public void issue24057NativeInputStreamSync() { "public InputStream download() throws ApiException {", "InputStream responseValue = localVarResponseBody;", "localVarResponseBody = null;", - "HttpRequest.BodyPublishers.ofInputStream(() -> body)" + "AtomicReference localVarRequestBody = new AtomicReference<>(body);", + "Supplier localVarRequestBodySupplier = () -> localVarRequestBody.getAndSet(null);", + "HttpRequest.BodyPublishers.ofInputStream(localVarRequestBodySupplier)" ); assertFileNotContains( apiFile, "File responseValue = downloadFileFromResponse(localVarResponse, localVarResponseBody);", - "memberVarObjectMapper.writeValueAsBytes(body)" + "memberVarObjectMapper.writeValueAsBytes(body)", + "HttpRequest.BodyPublishers.ofInputStream(() -> body)" ); } @@ -3897,12 +3907,19 @@ public void issue24057NativeInputStreamAsync() { "public CompletableFuture download() throws ApiException {", "InputStream responseValue = localVarResponseBody;", "localVarResponseBody = null;", - "HttpRequest.BodyPublishers.ofInputStream(() -> body)" + "localVarHttpInfoFuture.cancel(mayInterruptIfRunning);", + "if (!localVarResult.complete(localVarResponseBody) && localVarResponseBody != null)", + "CompletableFuture> localVarTransportFuture", + "localVarTransportFuture.cancel(mayInterruptIfRunning);", + "localVarProcessingFuture.whenComplete((localVarApiResponse, localVarThrowable) -> {", + "AtomicReference localVarRequestBody = new AtomicReference<>(body);", + "HttpRequest.BodyPublishers.ofInputStream(localVarRequestBodySupplier)" ); assertFileNotContains( apiFile, "File responseValue = downloadFileFromResponse(localVarResponse, localVarResponseBody);", - "memberVarObjectMapper.writeValueAsBytes(body)" + "memberVarObjectMapper.writeValueAsBytes(body)", + "HttpRequest.BodyPublishers.ofInputStream(() -> body)" ); } @@ -3932,8 +3949,40 @@ public void issue24057NativeNonBinaryInputStreamMappingKeepsJsonHandling() { assertFileNotContains( apiFile, "InputStream responseValue = localVarResponseBody;", - "HttpRequest.BodyPublishers.ofInputStream(() -> inputStream)" + "AtomicReference localVarRequestBody = new AtomicReference<>(inputStream);" + ); + } + + @Test + public void issue24057NativeInputStreamAsyncRuntimeOwnership() throws Exception { + Path output = generateIssue24057NativeOutput(true, true, false); + + compileAndRunIssue24057RuntimeHarness(output, "async-ownership"); + } + + @Test + public void issue24057NativeInputStreamRequestPublisherIsOneShot() throws Exception { + Path output = generateIssue24057NativeOutput(true, true, false); + + compileAndRunIssue24057RuntimeHarness(output, "plain-replay"); + } + + @Test + public void issue24057NativeInputStreamGzipPublisherIsOneShot() throws Exception { + Path output = generateIssue24057NativeOutput(true, true, true); + Path apiFile = output.resolve("src/main/java/xyz/abcdef/api/DefaultApi.java"); + + assertFileContains( + apiFile, + "localVarRequestBuilder.header(\"Content-Encoding\", \"gzip\");", + "ApiClient.gzipRequestBody(localVarRequestBodySupplier)" + ); + assertFileNotContains( + apiFile, + "HttpRequest.BodyPublishers.ofInputStream(() -> body)", + "memberVarObjectMapper.writeValueAsBytes(body)" ); + compileAndRunIssue24057RuntimeHarness(output, "gzip-replay"); } private Path generateIssue24057Native(boolean asyncNative, boolean mapStreams) { @@ -3949,11 +3998,35 @@ private Path generateIssue24057Native( boolean asyncNative, Map typeMappings, Map schemaMappings) { + return generateIssue24057NativeOutput(asyncNative, typeMappings, schemaMappings, false) + .resolve("src/main/java/xyz/abcdef/api/DefaultApi.java"); + } + + private Path generateIssue24057NativeOutput( + boolean asyncNative, + boolean mapStreams, + boolean useGzipFeature) { + return generateIssue24057NativeOutput( + asyncNative, + mapStreams ? Map.of( + "file", "InputStream", + "binary", "InputStream") : Collections.emptyMap(), + Collections.emptyMap(), + useGzipFeature); + } + + private Path generateIssue24057NativeOutput( + boolean asyncNative, + Map typeMappings, + Map schemaMappings, + boolean useGzipFeature) { Path output = newTempFolder(); Map properties = new HashMap<>(); properties.put(CodegenConstants.API_PACKAGE, "xyz.abcdef.api"); properties.put("asyncNative", asyncNative); properties.put(JavaClientCodegen.SUPPORT_STREAMING, true); + properties.put("useGzipFeature", useGzipFeature); + properties.put("openApiNullable", false); CodegenConfigurator configurator = new CodegenConfigurator() .setGeneratorName(JAVA_GENERATOR) @@ -3973,7 +4046,58 @@ private Path generateIssue24057Native( } new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); - return output.resolve("src/main/java/xyz/abcdef/api/DefaultApi.java"); + return output; + } + + private void compileAndRunIssue24057RuntimeHarness(Path output, String scenario) throws Exception { + Path classes = Files.createDirectories(output.resolve("runtime-test-classes")); + Path harness = Paths.get("src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java"); + List sources; + try (java.util.stream.Stream generatedSources = Files.walk(output.resolve("src/main/java"))) { + sources = generatedSources + .filter(path -> path.toString().endsWith(".java")) + .map(Path::toFile) + .collect(Collectors.toCollection(ArrayList::new)); + } + sources.add(harness.toFile()); + sources.add(Paths.get("src/test/resources/3_0/java/native/Generated.java").toFile()); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "A JDK compiler is required for the generated-client runtime test"); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.ROOT, StandardCharsets.UTF_8)) { + Iterable units = fileManager.getJavaFileObjectsFromFiles(sources); + List options = List.of( + "--release", "11", + "-classpath", System.getProperty("java.class.path"), + "-d", classes.toString()); + boolean compiled = compiler.getTask(null, fileManager, diagnostics, options, null, units).call(); + String diagnosticText = diagnostics.getDiagnostics().stream() + .map(Object::toString) + .collect(Collectors.joining(System.lineSeparator())); + Assert.assertTrue(compiled, "Generated runtime harness did not compile:\n" + diagnosticText); + } + + try (URLClassLoader classLoader = new URLClassLoader( + new java.net.URL[]{classes.toUri().toURL()}, + getClass().getClassLoader())) { + Class harnessClass = Class.forName( + "xyz.abcdef.runtime.NativeInputStreamRuntimeHarness", + true, + classLoader); + try { + harnessClass.getMethod("run", String.class).invoke(null, scenario); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new AssertionError("Generated runtime harness failed", cause); + } + } } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/java/native/Generated.java b/modules/openapi-generator/src/test/resources/3_0/java/native/Generated.java new file mode 100644 index 000000000000..27a5e983e7db --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/native/Generated.java @@ -0,0 +1,25 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.annotation; + +public @interface Generated { + String[] value(); + + String date() default ""; + + String comments() default ""; +} diff --git a/modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java b/modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java new file mode 100644 index 000000000000..50869a6bb804 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java @@ -0,0 +1,419 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.abcdef.runtime; + +import com.fasterxml.jackson.databind.ObjectMapper; +import xyz.abcdef.ApiClient; +import xyz.abcdef.ApiResponse; +import xyz.abcdef.api.DefaultApi; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.GZIPInputStream; + +public final class NativeInputStreamRuntimeHarness { + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final byte[] PAYLOAD = "issue-24057-payload".getBytes(StandardCharsets.UTF_8); + + private NativeInputStreamRuntimeHarness() {} + + public static void run(String scenario) throws Exception { + switch (scenario) { + case "async-ownership": + asyncConvenienceCancellationBeforeCompletionClosesStream(); + asyncWithHttpInfoCancellationDuringProcessingClosesStream(); + asyncSuccessLeavesStreamsCallerOwned(); + return; + case "plain-replay": + requestPublisherIsOneShot(false); + return; + case "gzip-replay": + requestPublisherIsOneShot(true); + return; + default: + throw new AssertionError("Unknown scenario: " + scenario); + } + } + + private static void asyncConvenienceCancellationBeforeCompletionClosesStream() throws Exception { + TestContext context = new TestContext(null); + TrackingInputStream responseBody = new TrackingInputStream(PAYLOAD); + + CompletableFuture returned = context.api.download(); + check(returned.cancel(true), "convenience future did not accept cancellation"); + await(context.httpClient.transport.cancelRequested, "convenience cancellation did not reach transport"); + + check(context.httpClient.transport.complete(response(responseBody)), "transport did not complete"); + await(responseBody.closed, "convenience cancellation leaked the undeliverable response stream"); + check(responseBody.closeCount.get() == 1, "convenience cancellation closed the stream more than once"); + } + + private static void asyncWithHttpInfoCancellationDuringProcessingClosesStream() throws Exception { + CountDownLatch processingStarted = new CountDownLatch(1); + CountDownLatch continueProcessing = new CountDownLatch(1); + TestContext context = new TestContext(ignored -> { + processingStarted.countDown(); + awaitUnchecked(continueProcessing, "processing was not released"); + }); + TrackingInputStream responseBody = new TrackingInputStream(PAYLOAD); + + CompletableFuture> returned = context.api.downloadWithHttpInfo(); + check(context.httpClient.transport.complete(response(responseBody)), "transport did not complete"); + await(processingStarted, "async response processing did not start"); + + check(returned.cancel(true), "WithHttpInfo future did not accept cancellation"); + await(context.httpClient.transport.cancelRequested, "WithHttpInfo cancellation did not reach transport"); + continueProcessing.countDown(); + + await(responseBody.closed, "WithHttpInfo cancellation leaked the processed response stream"); + check(responseBody.closeCount.get() == 1, "WithHttpInfo cancellation closed the stream more than once"); + } + + private static void asyncSuccessLeavesStreamsCallerOwned() throws Exception { + TestContext convenienceContext = new TestContext(null); + TrackingInputStream convenienceBody = new TrackingInputStream(PAYLOAD); + CompletableFuture convenienceResult = convenienceContext.api.download(); + convenienceContext.httpClient.transport.complete(response(convenienceBody)); + InputStream deliveredBody = convenienceResult.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + check(convenienceBody.closeCount.get() == 0, "convenience success closed the caller-owned stream"); + check(Arrays.equals(PAYLOAD, deliveredBody.readAllBytes()), "convenience success returned the wrong bytes"); + deliveredBody.close(); + check(convenienceBody.closeCount.get() == 1, "caller close was not observed for convenience result"); + + TestContext infoContext = new TestContext(null); + TrackingInputStream infoBody = new TrackingInputStream(PAYLOAD); + CompletableFuture> infoResult = infoContext.api.downloadWithHttpInfo(); + infoContext.httpClient.transport.complete(response(infoBody)); + InputStream deliveredInfoBody = infoResult.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS).getData(); + check(infoBody.closeCount.get() == 0, "WithHttpInfo success closed the caller-owned stream"); + check(Arrays.equals(PAYLOAD, deliveredInfoBody.readAllBytes()), "WithHttpInfo success returned the wrong bytes"); + deliveredInfoBody.close(); + check(infoBody.closeCount.get() == 1, "caller close was not observed for WithHttpInfo result"); + } + + private static void requestPublisherIsOneShot(boolean gzip) throws Exception { + TestContext context = new TestContext(null); + TrackingInputStream requestBody = new TrackingInputStream(PAYLOAD); + context.api.upload(requestBody); + + HttpRequest request = context.httpClient.request; + check(request != null, "upload request was not captured"); + HttpRequest.BodyPublisher publisher = request.bodyPublisher() + .orElseThrow(() -> new AssertionError("upload request has no body publisher")); + + CollectingSubscriber first = new CollectingSubscriber(); + publisher.subscribe(first); + byte[] published = first.success(); + byte[] decoded = gzip ? gunzip(published) : published; + check(Arrays.equals(PAYLOAD, decoded), "first subscription published the wrong payload"); + await(requestBody.closed, "first subscription did not close the source"); + int closesAfterFirstSubscription = requestBody.closeCount.get(); + check(closesAfterFirstSubscription > 0, "first subscription did not close the source"); + if (gzip) { + check(closesAfterFirstSubscription == 1, "gzip first subscription did not close the source exactly once"); + } + + CollectingSubscriber second = new CollectingSubscriber(); + publisher.subscribe(second); + Throwable repeatFailure = second.failure(); + check(repeatFailure != null, "second subscription reused a one-shot InputStream"); + check(requestBody.closeCount.get() == closesAfterFirstSubscription, + "second subscription touched the consumed source stream"); + + if (gzip) { + check("gzip".equals(request.headers().firstValue("Content-Encoding").orElse(null)), + "gzip request did not set Content-Encoding"); + } + } + + private static byte[] gunzip(byte[] compressed) throws IOException { + try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(compressed))) { + return gzip.readAllBytes(); + } + } + + private static HttpResponse response(InputStream body) { + return new TestResponse(body); + } + + private static void await(CountDownLatch latch, String message) throws InterruptedException { + check(latch.await(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS), message); + } + + private static void awaitUnchecked(CountDownLatch latch, String message) { + try { + await(latch, message); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(message, e); + } + } + + private static void check(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } + + private static final class TestContext { + private final ControlledHttpClient httpClient = new ControlledHttpClient(); + private final DefaultApi api; + + private TestContext(java.util.function.Consumer> asyncInterceptor) { + ApiClient client = new ApiClient(new ControlledBuilder(httpClient), new ObjectMapper(), "http://localhost"); + client.setAsyncResponseInterceptor(asyncInterceptor); + api = new DefaultApi(client); + } + } + + private static final class TrackingInputStream extends ByteArrayInputStream { + private final AtomicInteger closeCount = new AtomicInteger(); + private final CountDownLatch closed = new CountDownLatch(1); + + private TrackingInputStream(byte[] bytes) { + super(bytes); + } + + @Override + public void close() throws IOException { + closeCount.incrementAndGet(); + closed.countDown(); + super.close(); + } + } + + private static final class NonCancellingFuture extends CompletableFuture { + private final CountDownLatch cancelRequested = new CountDownLatch(1); + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + cancelRequested.countDown(); + return false; + } + } + + private static final class ControlledHttpClient extends HttpClient { + private final NonCancellingFuture> transport = new NonCancellingFuture<>(); + private volatile HttpRequest request; + + @Override + public Optional cookieHandler() { + return Optional.empty(); + } + + @Override + public Optional connectTimeout() { + return Optional.empty(); + } + + @Override + public Redirect followRedirects() { + return Redirect.NEVER; + } + + @Override + public Optional proxy() { + return Optional.empty(); + } + + @Override + public SSLContext sslContext() { + return null; + } + + @Override + public SSLParameters sslParameters() { + return new SSLParameters(); + } + + @Override + public Optional authenticator() { + return Optional.empty(); + } + + @Override + public Version version() { + return Version.HTTP_1_1; + } + + @Override + public Optional executor() { + return Optional.empty(); + } + + @Override + public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler handler) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public CompletableFuture> sendAsync( + HttpRequest request, HttpResponse.BodyHandler handler) { + this.request = request; + return (CompletableFuture>) (CompletableFuture) transport; + } + + @Override + public CompletableFuture> sendAsync( + HttpRequest request, + HttpResponse.BodyHandler handler, + HttpResponse.PushPromiseHandler pushPromiseHandler) { + return sendAsync(request, handler); + } + } + + private static final class ControlledBuilder implements HttpClient.Builder { + private final HttpClient client; + + private ControlledBuilder(HttpClient client) { + this.client = client; + } + + @Override + public HttpClient.Builder cookieHandler(CookieHandler cookieHandler) { return this; } + + @Override + public HttpClient.Builder connectTimeout(Duration duration) { return this; } + + @Override + public HttpClient.Builder sslContext(SSLContext sslContext) { return this; } + + @Override + public HttpClient.Builder sslParameters(SSLParameters sslParameters) { return this; } + + @Override + public HttpClient.Builder executor(Executor executor) { return this; } + + @Override + public HttpClient.Builder followRedirects(HttpClient.Redirect policy) { return this; } + + @Override + public HttpClient.Builder version(HttpClient.Version version) { return this; } + + @Override + public HttpClient.Builder priority(int priority) { return this; } + + @Override + public HttpClient.Builder proxy(ProxySelector proxySelector) { return this; } + + @Override + public HttpClient.Builder authenticator(Authenticator authenticator) { return this; } + + @Override + public HttpClient build() { return client; } + } + + private static final class TestResponse implements HttpResponse { + private final InputStream body; + + private TestResponse(InputStream body) { + this.body = body; + } + + @Override + public int statusCode() { return 200; } + + @Override + public HttpRequest request() { return HttpRequest.newBuilder(URI.create("http://localhost/download")).build(); } + + @Override + public Optional> previousResponse() { return Optional.empty(); } + + @Override + public HttpHeaders headers() { return HttpHeaders.of(java.util.Map.of(), (name, value) -> true); } + + @Override + public InputStream body() { return body; } + + @Override + public Optional sslSession() { return Optional.empty(); } + + @Override + public URI uri() { return URI.create("http://localhost/download"); } + + @Override + public HttpClient.Version version() { return HttpClient.Version.HTTP_1_1; } + } + + private static final class CollectingSubscriber implements Flow.Subscriber { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private final CompletableFuture result = new CompletableFuture<>(); + + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer item) { + byte[] chunk = new byte[item.remaining()]; + item.get(chunk); + bytes.write(chunk, 0, chunk.length); + } + + @Override + public void onError(Throwable throwable) { + result.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + result.complete(bytes.toByteArray()); + } + + private byte[] success() throws Exception { + return result.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + + private Throwable failure() throws Exception { + try { + result.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + return null; + } catch (java.util.concurrent.ExecutionException e) { + return e.getCause(); + } catch (CompletionException e) { + return e.getCause(); + } + } + } +} From a5b40286bcc3a3bf66a9498dc3345f4de8ab85f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Tue, 14 Jul 2026 10:58:31 +0900 Subject: [PATCH 4/5] Prove convenience stream cleanup at the delivery boundary The cancellation runtime test previously observed cleanup through the inner WithHttpInfo bridge, leaving the convenience future's own lost-delivery branch unexecuted. Encapsulate convenience delivery in its returned future and deterministically invoke that exact path after cancellation with a close-tracking stream. Constraint: The normal cancellation path also cancels WithHttpInfo, so the outer complete(false) race cannot be isolated by transport timing alone. Rejected: Rely on source assertions for the outer branch | they do not execute ownership cleanup. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep convenience response delivery routed through completeResponse so its undeliverable-stream close remains runtime-testable. Tested: Isolated async ownership runtime test RED on missing delivery method, then GREEN with tests=1 failures=0 errors=0; generated source compiled dynamically at Java 11 release. Not-tested: Full repository test suite; live network transport. --- .../Java/libraries/native/api.mustache | 23 +++++++++++------- .../codegen/java/JavaClientCodegenTest.java | 3 ++- .../NativeInputStreamRuntimeHarness.java | 24 +++++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache index 50bf854eec66..d586869d3066 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/api.mustache @@ -386,7 +386,7 @@ public class {{classname}} { {{#vendorExtensions.x-java-native-response-input-stream}} CompletableFuture> localVarHttpInfoFuture = {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); - CompletableFuture<{{{returnType}}}> localVarResult = new CompletableFuture<{{{returnType}}}>() { + class LocalVarResultFuture extends CompletableFuture<{{{returnType}}}> { @Override public boolean cancel(boolean mayInterruptIfRunning) { boolean localVarCancelled = super.cancel(mayInterruptIfRunning); @@ -395,19 +395,26 @@ public class {{classname}} { } return localVarCancelled; } - }; - localVarHttpInfoFuture.whenComplete((localVarApiResponse, localVarThrowable) -> { - if (localVarThrowable != null) { - localVarResult.completeExceptionally(localVarThrowable); - } else { - {{{returnType}}} localVarResponseBody = localVarApiResponse.getData(); - if (!localVarResult.complete(localVarResponseBody) && localVarResponseBody != null) { + + boolean completeResponse({{{returnType}}} localVarResponseBody) { + boolean localVarCompleted = super.complete(localVarResponseBody); + if (!localVarCompleted && localVarResponseBody != null) { try { localVarResponseBody.close(); } catch (IOException ignored) { // The result is already terminal, so cleanup failure has no receiver. } } + return localVarCompleted; + } + } + LocalVarResultFuture localVarResult = new LocalVarResultFuture(); + localVarHttpInfoFuture.whenComplete((localVarApiResponse, localVarThrowable) -> { + if (localVarThrowable != null) { + localVarResult.completeExceptionally(localVarThrowable); + } else { + {{{returnType}}} localVarResponseBody = localVarApiResponse.getData(); + localVarResult.completeResponse(localVarResponseBody); } }); return localVarResult; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index ca0e1b7633ce..5449bd5293cc 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -3908,7 +3908,8 @@ public void issue24057NativeInputStreamAsync() { "InputStream responseValue = localVarResponseBody;", "localVarResponseBody = null;", "localVarHttpInfoFuture.cancel(mayInterruptIfRunning);", - "if (!localVarResult.complete(localVarResponseBody) && localVarResponseBody != null)", + "boolean completeResponse(InputStream localVarResponseBody)", + "localVarResult.completeResponse(localVarResponseBody);", "CompletableFuture> localVarTransportFuture", "localVarTransportFuture.cancel(mayInterruptIfRunning);", "localVarProcessingFuture.whenComplete((localVarApiResponse, localVarThrowable) -> {", diff --git a/modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java b/modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java index 50869a6bb804..ee34e0a43ad0 100644 --- a/modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java +++ b/modules/openapi-generator/src/test/resources/3_0/java/native/NativeInputStreamRuntimeHarness.java @@ -28,6 +28,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.Authenticator; import java.net.CookieHandler; import java.net.ProxySelector; @@ -82,11 +84,33 @@ private static void asyncConvenienceCancellationBeforeCompletionClosesStream() t check(returned.cancel(true), "convenience future did not accept cancellation"); await(context.httpClient.transport.cancelRequested, "convenience cancellation did not reach transport"); + TrackingInputStream outerUndeliverableBody = new TrackingInputStream(PAYLOAD); + check(!completeConvenienceResponse(returned, outerUndeliverableBody), + "cancelled convenience future accepted a late response stream"); + await(outerUndeliverableBody.closed, "convenience delivery bridge did not close its late response stream"); + check(outerUndeliverableBody.closeCount.get() == 1, + "convenience delivery bridge closed its late response stream more than once"); + check(context.httpClient.transport.complete(response(responseBody)), "transport did not complete"); await(responseBody.closed, "convenience cancellation leaked the undeliverable response stream"); check(responseBody.closeCount.get() == 1, "convenience cancellation closed the stream more than once"); } + private static boolean completeConvenienceResponse( + CompletableFuture returned, InputStream responseBody) throws Exception { + Method completeResponse = returned.getClass().getDeclaredMethod("completeResponse", InputStream.class); + completeResponse.setAccessible(true); + try { + return (Boolean) completeResponse.invoke(returned, responseBody); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new AssertionError("convenience delivery bridge failed", cause); + } + } + private static void asyncWithHttpInfoCancellationDuringProcessingClosesStream() throws Exception { CountDownLatch processingStarted = new CountDownLatch(1); CountDownLatch continueProcessing = new CountDownLatch(1); From d21fad37bfd224db5cb2c7c2714d06d2b7645c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Mon, 20 Jul 2026 10:29:20 +0900 Subject: [PATCH 5/5] Synchronize Java native InputStream samples The sample check regenerates native clients from the current Java templates. Commit the generated clients so checked-in examples cover direct InputStream response and request paths. Constraint: Samples must be generated by the repository CLI. Rejected: Hand-edit samples | would drift from templates. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Re-run bin/generate-samples.sh after changing Java native templates. Tested: openapi-generator-cli package; bin/generate-samples.sh (775 generators); git diff --check Not-tested: Focused unit test not rerun in this sample-only update Related: #24057 --- .../main/java/org/openapitools/client/api/AuthApi.java | 2 ++ .../main/java/org/openapitools/client/api/BodyApi.java | 2 ++ .../main/java/org/openapitools/client/api/FormApi.java | 2 ++ .../main/java/org/openapitools/client/api/HeaderApi.java | 2 ++ .../main/java/org/openapitools/client/api/PathApi.java | 2 ++ .../main/java/org/openapitools/client/api/QueryApi.java | 2 ++ .../java/org/openapitools/client/api/AnotherFakeApi.java | 2 ++ .../java/org/openapitools/client/api/DefaultApi.java | 2 ++ .../main/java/org/openapitools/client/api/FakeApi.java | 2 ++ .../openapitools/client/api/FakeClassnameTags123Api.java | 2 ++ .../main/java/org/openapitools/client/api/PetApi.java | 2 ++ .../main/java/org/openapitools/client/api/StoreApi.java | 2 ++ .../main/java/org/openapitools/client/api/UserApi.java | 2 ++ .../java/org/openapitools/client/api/DefaultApi.java | 2 ++ .../java/org/openapitools/client/api/AnotherFakeApi.java | 2 ++ .../java/org/openapitools/client/api/DefaultApi.java | 2 ++ .../main/java/org/openapitools/client/api/FakeApi.java | 2 ++ .../openapitools/client/api/FakeClassnameTags123Api.java | 2 ++ .../main/java/org/openapitools/client/api/PetApi.java | 2 ++ .../main/java/org/openapitools/client/api/StoreApi.java | 2 ++ .../main/java/org/openapitools/client/api/UserApi.java | 2 ++ .../main/java/org/openapitools/client/api/PetApi.java | 2 ++ .../main/java/org/openapitools/client/api/StoreApi.java | 2 ++ .../main/java/org/openapitools/client/api/UserApi.java | 2 ++ .../src/main/java/org/openapitools/client/ApiClient.java | 9 +++++++-- .../main/java/org/openapitools/client/api/PetApi.java | 1 + .../java/org/openapitools/client/api/AnotherFakeApi.java | 2 ++ .../java/org/openapitools/client/api/DefaultApi.java | 2 ++ .../main/java/org/openapitools/client/api/FakeApi.java | 2 ++ .../openapitools/client/api/FakeClassnameTags123Api.java | 2 ++ .../main/java/org/openapitools/client/api/PetApi.java | 2 ++ .../main/java/org/openapitools/client/api/StoreApi.java | 2 ++ .../main/java/org/openapitools/client/api/UserApi.java | 2 ++ 33 files changed, 70 insertions(+), 2 deletions(-) diff --git a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/AuthApi.java b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/AuthApi.java index e182b6cdd9b5..85f93bf471c2 100644 --- a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/AuthApi.java +++ b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/AuthApi.java @@ -43,6 +43,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class AuthApi { diff --git a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/BodyApi.java b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/BodyApi.java index 503709f81716..9d841051d03a 100644 --- a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/BodyApi.java +++ b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/BodyApi.java @@ -53,6 +53,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class BodyApi { diff --git a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/FormApi.java b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/FormApi.java index 23ec0c80b8bf..cafe956a863d 100644 --- a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/FormApi.java +++ b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/FormApi.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class FormApi { diff --git a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/HeaderApi.java b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/HeaderApi.java index 7760b368f954..b20c45ab89b0 100644 --- a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/HeaderApi.java +++ b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/HeaderApi.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class HeaderApi { diff --git a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/PathApi.java b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/PathApi.java index 8bdcccd34e61..931709a55ca7 100644 --- a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/PathApi.java +++ b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/PathApi.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class PathApi { diff --git a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/QueryApi.java b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/QueryApi.java index 44d00d8ae3af..88f2626c89b2 100644 --- a/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/QueryApi.java +++ b/samples/client/echo_api/java/native/src/main/java/org/openapitools/client/api/QueryApi.java @@ -56,6 +56,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class QueryApi { diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/AnotherFakeApi.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/AnotherFakeApi.java index 5f334eb8a272..874cb41841ec 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/AnotherFakeApi.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/AnotherFakeApi.java @@ -44,8 +44,10 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class AnotherFakeApi { diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/DefaultApi.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/DefaultApi.java index 0fbc1b125d58..68dad668a7f2 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/DefaultApi.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/DefaultApi.java @@ -44,8 +44,10 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class DefaultApi { diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeApi.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeApi.java index e95b751bbe00..1d2d14162e77 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeApi.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeApi.java @@ -61,8 +61,10 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class FakeApi { diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java index 6ec41e678438..f2df41a3570f 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java @@ -50,8 +50,10 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class FakeClassnameTags123Api { diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/PetApi.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/PetApi.java index 855654446dc6..91689b2f72b1 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/PetApi.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/PetApi.java @@ -52,8 +52,10 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class PetApi { diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/StoreApi.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/StoreApi.java index 30dc59fcb2d4..dee09dae188f 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/StoreApi.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/StoreApi.java @@ -50,8 +50,10 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class StoreApi { diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/UserApi.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/UserApi.java index 70f7946f58be..0b129c90ac5b 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/UserApi.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/api/UserApi.java @@ -51,8 +51,10 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class UserApi { diff --git a/samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/DefaultApi.java b/samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/DefaultApi.java index bcade2528929..6f9f5f2f48fe 100644 --- a/samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/DefaultApi.java +++ b/samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/DefaultApi.java @@ -54,6 +54,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class DefaultApi { diff --git a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/AnotherFakeApi.java b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/AnotherFakeApi.java index eab874144668..775f2f54ed9b 100644 --- a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/AnotherFakeApi.java +++ b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/AnotherFakeApi.java @@ -45,6 +45,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class AnotherFakeApi { diff --git a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/DefaultApi.java b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/DefaultApi.java index 2246ad726ab5..e307cc605658 100644 --- a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/DefaultApi.java +++ b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/DefaultApi.java @@ -45,6 +45,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class DefaultApi { diff --git a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeApi.java b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeApi.java index 3dbea286e2d4..3aab15f56ee7 100644 --- a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeApi.java +++ b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeApi.java @@ -62,6 +62,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class FakeApi { diff --git a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java index 6a6295c78ee4..d3957cbce569 100644 --- a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java +++ b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java @@ -51,6 +51,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class FakeClassnameTags123Api { diff --git a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/PetApi.java b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/PetApi.java index 2da350c5e9f9..1075c96d2af1 100644 --- a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/PetApi.java +++ b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/PetApi.java @@ -53,6 +53,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class PetApi { diff --git a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/StoreApi.java b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/StoreApi.java index 4ba985b12d3d..b61b1e5c9570 100644 --- a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/StoreApi.java +++ b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/StoreApi.java @@ -51,6 +51,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class StoreApi { diff --git a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/UserApi.java b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/UserApi.java index fd02c8d9e4de..ed13befdf137 100644 --- a/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/UserApi.java +++ b/samples/client/petstore/java/native-jackson3/src/main/java/org/openapitools/client/api/UserApi.java @@ -52,6 +52,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class UserApi { diff --git a/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/PetApi.java b/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/PetApi.java index d0bbc241009c..153b9b5a0e76 100644 --- a/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/PetApi.java +++ b/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/PetApi.java @@ -52,6 +52,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class PetApi { diff --git a/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/StoreApi.java b/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/StoreApi.java index adec2bbdba1b..b51d2c24b669 100644 --- a/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/StoreApi.java +++ b/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/StoreApi.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class StoreApi { diff --git a/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/UserApi.java b/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/UserApi.java index 3219c311a56f..4a6b74b7f2a6 100644 --- a/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/UserApi.java +++ b/samples/client/petstore/java/native-jakarta/src/main/java/org/openapitools/client/api/UserApi.java @@ -51,6 +51,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class UserApi { diff --git a/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/ApiClient.java index 0e8af53e8374..f9111870b1d7 100644 --- a/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/ApiClient.java @@ -498,7 +498,10 @@ public static InputStream getResponseBody(HttpResponse response) th */ public static HttpRequest.BodyPublisher gzipRequestBody(Supplier bodySupplier) { Objects.requireNonNull(bodySupplier, "bodySupplier must not be null"); - return HttpRequest.BodyPublishers.ofInputStream(() -> new GzipCompressingInputStream(bodySupplier)); + return HttpRequest.BodyPublishers.ofInputStream(() -> { + InputStream body = bodySupplier.get(); + return body == null ? null : new GzipCompressingInputStream(() -> body); + }); } private static final class GzipCompressingInputStream extends InputStream { @@ -525,15 +528,17 @@ private void ensureInitialized() throws IOException { private boolean fillBuffer() throws IOException { while (chunkPosition >= currentChunk.length) { buffer.reset(); - ensureInitialized(); if (finished) { return false; } + ensureInitialized(); int bytesRead = source.read(readBuffer); if (bytesRead == -1) { gzipStream.finish(); gzipStream.close(); + gzipStream = null; source.close(); + source = null; finished = true; } else { gzipStream.write(readBuffer, 0, bytesRead); diff --git a/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/api/PetApi.java b/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/api/PetApi.java index b8b6d5582a58..528b9aac9587 100644 --- a/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/api/PetApi.java +++ b/samples/client/petstore/java/native-useGzipFeature/src/main/java/org/openapitools/client/api/PetApi.java @@ -45,6 +45,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class PetApi { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/AnotherFakeApi.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/AnotherFakeApi.java index 5e6ed31760f2..d0313c52ace5 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/AnotherFakeApi.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/AnotherFakeApi.java @@ -44,6 +44,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class AnotherFakeApi { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/DefaultApi.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/DefaultApi.java index 1b708412f7a8..b05b53578893 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/DefaultApi.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/DefaultApi.java @@ -44,6 +44,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class DefaultApi { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeApi.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeApi.java index 21d981ed0f30..afedb78bd578 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeApi.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeApi.java @@ -61,6 +61,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class FakeApi { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java index b4a465bad32c..579b9d9009fb 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class FakeClassnameTags123Api { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/PetApi.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/PetApi.java index 5fa519ecd921..c107596d3123 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/PetApi.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/PetApi.java @@ -52,6 +52,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class PetApi { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/StoreApi.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/StoreApi.java index 8834fee603cd..886a37b89ef0 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/StoreApi.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/StoreApi.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class StoreApi { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/UserApi.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/UserApi.java index dab98c135229..df7843c08645 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/UserApi.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/api/UserApi.java @@ -51,6 +51,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.24.0-SNAPSHOT") public class UserApi {