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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion azure-functions-java-core-library/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.azure.functions</groupId>
<artifactId>azure-functions-java-core-library</artifactId>
<version>1.3.0</version>
<version>1.4.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>com.microsoft.maven</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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 <T> the type of the input to the operation
* @since 1.4.0
*/
@FunctionalInterface
interface IOConsumer<T> {
/**
* 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
*/
Expand Down Expand Up @@ -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.
*
* <p>The stream is closed by the runtime after the response has been
* sent. Implementations should not assume the stream supports
* {@code mark}/{@code reset}.</p>
*
* <p>This is a typed alias for {@link #body(Object)} that signals to the
* runtime to use the streaming write path.</p>
*
* @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.
*
* <p>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}.</p>
*
* <p>This is a typed alias for {@link #body(Object)} that signals to the
* runtime to use the streaming write path.</p>
*
* @param writer callback invoked with the response output stream
* @return this builder
* @since 1.4.0
*/
default Builder bodyStream(IOConsumer<OutputStream> writer) {
return body(writer);
}

/**
* Creates an instance of HttpMessageResponse with the values configured in this builder.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OutputStream> writer = os -> os.write(42);

Builder returned = builder.bodyStream(writer);

assertSame(builder, returned);
assertSame("bodyStream(IOConsumer<OutputStream>) must pass the writer through to body(Object) unchanged",
writer, builder.lastBody);
}

@Test
public void ioConsumerPropagatesIOException() {
IOConsumer<OutputStream> 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<OutputStream> 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<String, String> 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;
}
}
}