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; + } + } +}