From bea0523664a96965c028abe89104a3990862e0a0 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Wed, 3 Jun 2026 14:46:13 -0500 Subject: [PATCH] Add streaming bodyStream() overloads to HttpResponseMessage.Builder Adds two new default methods to HttpResponseMessage.Builder for streaming HTTP responses without buffering the entire payload in memory: Builder bodyStream(InputStream stream) Builder bodyStream(IOConsumer writer) Both methods delegate to body(Object), so existing runtimes that do not implement the streaming write path continue to work via their existing type dispatch on body. Runtimes that do implement streaming (e.g. the Java worker's HTTP proxy path) detect InputStream / IOConsumer on the body and write incrementally to the HTTP response. Also adds the IOConsumer functional interface (throwing IOException), since java.util.function.Consumer does not allow checked exceptions and streaming writers commonly throw IOException. Version bumped: 1.3.0 -> 1.4.0-SNAPSHOT (the corresponding worker change consumes this SNAPSHOT until release). --- azure-functions-java-core-library/pom.xml | 2 +- .../azure/functions/HttpResponseMessage.java | 67 +++++++++- .../HttpResponseMessageBuilderTest.java | 121 ++++++++++++++++++ 3 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 azure-functions-java-core-library/src/test/java/com/microsoft/azure/functions/HttpResponseMessageBuilderTest.java diff --git a/azure-functions-java-core-library/pom.xml b/azure-functions-java-core-library/pom.xml index 3ce2b4c..0f67c68 100644 --- a/azure-functions-java-core-library/pom.xml +++ b/azure-functions-java-core-library/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.microsoft.azure.functions azure-functions-java-core-library - 1.3.0 + 1.4.0-SNAPSHOT jar com.microsoft.maven diff --git a/azure-functions-java-core-library/src/main/java/com/microsoft/azure/functions/HttpResponseMessage.java b/azure-functions-java-core-library/src/main/java/com/microsoft/azure/functions/HttpResponseMessage.java index 67961dd..96df286 100644 --- a/azure-functions-java-core-library/src/main/java/com/microsoft/azure/functions/HttpResponseMessage.java +++ b/azure-functions-java-core-library/src/main/java/com/microsoft/azure/functions/HttpResponseMessage.java @@ -6,6 +6,10 @@ package com.microsoft.azure.functions; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + /** * An HttpResponseMessage instance is returned by Azure Functions methods that are triggered by an * {https://github.com/Azure/azure-functions-java-library/blob/dev/src/main/java/com/microsoft/azure/functions/annotation/HttpTrigger.java}. @@ -46,7 +50,25 @@ default int getStatusCode() { * @return the body of the HTTP response. */ Object getBody(); - + + /** + * A consumer that may throw {@link IOException}, used by + * {@link Builder#bodyStream(IOConsumer)} for callback-driven response streaming. + * + * @param the type of the input to the operation + * @since 1.4.0 + */ + @FunctionalInterface + interface IOConsumer { + /** + * Performs this operation on the given argument. + * + * @param value the input argument + * @throws IOException if an I/O error occurs + */ + void accept(T value) throws IOException; + } + /** * A builder to create an instance of HttpResponseMessage */ @@ -80,6 +102,49 @@ public interface Builder { */ Builder body(Object body); + /** + * Streams the body of the HTTP response from an {@link InputStream}. The + * stream is read by the Functions runtime and copied to the response body + * without buffering the entire payload in memory; suitable for large + * payloads or content of unknown length. + * + *

The stream is closed by the runtime after the response has been + * sent. Implementations should not assume the stream supports + * {@code mark}/{@code reset}.

+ * + *

This is a typed alias for {@link #body(Object)} that signals to the + * runtime to use the streaming write path.

+ * + * @param stream the input stream to stream as the response body + * @return this builder + * @since 1.4.0 + */ + default Builder bodyStream(InputStream stream) { + return body(stream); + } + + /** + * Streams the body of the HTTP response via a writer callback. The + * Functions runtime invokes the callback with the response + * {@link OutputStream} once response headers have been sent; the + * function writes its content to the stream and returns. The runtime + * flushes and closes the stream when the callback returns. + * + *

Use this overload for server-sent events, chunked responses, or + * any payload that is more naturally produced incrementally than + * materialized as a single {@code byte[]} or {@code InputStream}.

+ * + *

This is a typed alias for {@link #body(Object)} that signals to the + * runtime to use the streaming write path.

+ * + * @param writer callback invoked with the response output stream + * @return this builder + * @since 1.4.0 + */ + default Builder bodyStream(IOConsumer writer) { + return body(writer); + } + /** * Creates an instance of HttpMessageResponse with the values configured in this builder. * diff --git a/azure-functions-java-core-library/src/test/java/com/microsoft/azure/functions/HttpResponseMessageBuilderTest.java b/azure-functions-java-core-library/src/test/java/com/microsoft/azure/functions/HttpResponseMessageBuilderTest.java new file mode 100644 index 0000000..4e97385 --- /dev/null +++ b/azure-functions-java-core-library/src/test/java/com/microsoft/azure/functions/HttpResponseMessageBuilderTest.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.functions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import com.microsoft.azure.functions.HttpResponseMessage.Builder; +import com.microsoft.azure.functions.HttpResponseMessage.IOConsumer; + +import org.junit.Test; + +/** + * Verifies the default {@code bodyStream} overloads on + * {@link HttpResponseMessage.Builder} delegate to {@link Builder#body(Object)} + * with the original object reference preserved, so the runtime can + * type-dispatch on it. + */ +public class HttpResponseMessageBuilderTest { + + @Test + public void bodyStreamInputStreamDelegatesToBody() { + RecordingBuilder builder = new RecordingBuilder(); + InputStream stream = new ByteArrayInputStream(new byte[]{1, 2, 3}); + + Builder returned = builder.bodyStream(stream); + + assertSame("bodyStream should be a fluent builder", builder, returned); + assertSame("bodyStream(InputStream) must pass the stream through to body(Object) unchanged", + stream, builder.lastBody); + } + + @Test + public void bodyStreamConsumerDelegatesToBody() { + RecordingBuilder builder = new RecordingBuilder(); + IOConsumer writer = os -> os.write(42); + + Builder returned = builder.bodyStream(writer); + + assertSame(builder, returned); + assertSame("bodyStream(IOConsumer) must pass the writer through to body(Object) unchanged", + writer, builder.lastBody); + } + + @Test + public void ioConsumerPropagatesIOException() { + IOConsumer writer = os -> { + throw new IOException("disk full"); + }; + + try { + writer.accept(new ByteArrayOutputStream()); + fail("Expected IOException"); + } catch (IOException ex) { + assertEquals("disk full", ex.getMessage()); + } + } + + @Test + public void ioConsumerExecutesNormally() throws Exception { + IOConsumer writer = os -> os.write("hi".getBytes("UTF-8")); + ByteArrayOutputStream sink = new ByteArrayOutputStream(); + writer.accept(sink); + assertEquals("hi", new String(sink.toByteArray(), "UTF-8")); + } + + @Test + public void bodyStreamRetainsAllOtherBuilderState() { + RecordingBuilder builder = new RecordingBuilder(); + builder.status(HttpStatus.ACCEPTED) + .header("X-Test", "1") + .bodyStream(new ByteArrayInputStream(new byte[0])); + + assertEquals(HttpStatus.ACCEPTED, builder.lastStatus); + assertEquals("1", builder.headers.get("X-Test")); + } + + /** Minimal in-memory Builder that records the last body passed in. */ + private static final class RecordingBuilder implements Builder { + Object lastBody; + HttpStatusType lastStatus; + Map headers = new HashMap<>(); + + @Override + public Builder status(HttpStatusType status) { + this.lastStatus = status; + return this; + } + + @Override + public Builder header(String key, String value) { + this.headers.put(key, value); + return this; + } + + @Override + public Builder body(Object body) { + this.lastBody = body; + return this; + } + + @Override + public HttpResponseMessage build() { + // Tests inspect builder state directly; no need to materialize a response. + return null; + } + } +}