Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.openai.errors.OpenAIInvalidDataException
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
import java.io.SequenceInputStream
import java.util.Collections
import java.util.UUID
import kotlin.jvm.optionals.getOrNull

Expand All @@ -21,6 +23,8 @@ internal inline fun <reified T> json(jsonMapper: JsonMapper, value: T): HttpRequ

override fun writeTo(outputStream: OutputStream) = outputStream.write(bytes)

override fun content(): InputStream = bytes.inputStream()

override fun contentType(): String = "application/json"

override fun contentLength(): Long = bytes.size.toLong()
Expand Down Expand Up @@ -60,6 +64,8 @@ internal fun multipartFormData(
outputStream.write(byteArray)
}

override fun content(): InputStream = byteArray.inputStream()

override fun contentType(): String = field.contentType

override fun contentLength(): Long = byteArray.size.toLong()
Expand All @@ -75,6 +81,8 @@ internal fun multipartFormData(
bytes.copyTo(outputStream)
}

override fun content(): InputStream = bytes

override fun contentType(): String = field.contentType

override fun contentLength(): Long = -1L
Expand Down Expand Up @@ -147,6 +155,36 @@ private constructor(private val boundary: String, private val parts: List<Part>)
outputStream.write(CRLF)
}

// This must remain in sync with `writeTo`.
override fun content(): InputStream {
val streams = mutableListOf<InputStream>()

parts.forEach { part ->
streams.add(DASHDASH.inputStream())
streams.add(boundaryBytes.inputStream())
streams.add(CRLF.inputStream())

streams.add(CONTENT_DISPOSITION.inputStream())
streams.add(part.contentDisposition.toByteArray().inputStream())
streams.add(CRLF.inputStream())

streams.add(CONTENT_TYPE.inputStream())
streams.add(part.contentType.toByteArray().inputStream())
streams.add(CRLF.inputStream())

streams.add(CRLF.inputStream())
streams.add(part.body.content())
streams.add(CRLF.inputStream())
}

streams.add(DASHDASH.inputStream())
streams.add(boundaryBytes.inputStream())
streams.add(DASHDASH.inputStream())
streams.add(CRLF.inputStream())

return SequenceInputStream(Collections.enumeration(streams))
}

override fun contentType(): String = contentType

// This must remain in sync with `writeTo`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
package com.openai.core.http

import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.lang.AutoCloseable

interface HttpRequestBody : AutoCloseable {

fun writeTo(outputStream: OutputStream)

/**
* Returns the request body content as an input stream.
*
* The default implementation buffers the bytes produced by [writeTo] so existing third-party
* implementations remain compatible. Implementations backed by an existing byte array or stream
* should override this method to avoid buffering.
*/
fun content(): InputStream {
val outputStream = ByteArrayOutputStream()
writeTo(outputStream)
return outputStream.toByteArray().inputStream()
Comment on lines +19 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve streaming through the logging body wrapper

When LoggingHttpClient runs at DEBUG level, it replaces the request body with LoggingHttpRequestBody, which does not override content(). A custom HttpClient consuming that body through the new API therefore invokes this fallback, causing LoggingHttpRequestBody.writeTo() to materialize the entire body in a ByteArrayOutputStream before returning; large InputStream-backed multipart uploads consequently lose the streaming behavior introduced here and may exhaust the heap. The logging wrapper needs a streaming content() implementation that preserves its logging behavior.

Useful? React with 👍 / 👎.

}

fun contentType(): String?

fun contentLength(): Long
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.openai.core.http;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;

final class HttpRequestBodyJavaTest {

@Test
void contentIsJavaDefaultMethod() throws Exception {
assertThat(HttpRequestBody.class.getMethod("content").isDefault()).isTrue();

HttpRequestBody body =
new HttpRequestBody() {
@Override
public void writeTo(OutputStream outputStream) {
try {
outputStream.write("body".getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public String contentType() {
return "text/plain";
}

@Override
public long contentLength() {
return 4L;
}

@Override
public boolean repeatable() {
return true;
}

@Override
public void close() {}
};

try (InputStream content = body.content()) {
byte[] bytes = new byte[4];
assertThat(content.read(bytes)).isEqualTo(bytes.length);
assertThat(bytes).isEqualTo("body".getBytes(StandardCharsets.UTF_8));
assertThat(content.read()).isEqualTo(-1);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.openai.core.http

import com.openai.core.MultipartField
import com.openai.core.jsonMapper
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class HttpRequestBodyContentTest {

@Test
fun content_defaultsToWriteTo() {
val body =
object : HttpRequestBody {
override fun writeTo(outputStream: OutputStream) {
outputStream.write("body".toByteArray())
}

override fun contentType(): String = "text/plain"

override fun contentLength(): Long = 4L

override fun repeatable(): Boolean = true

override fun close() {}
}

body.content().use { content -> assertThat(content.readBytes()).isEqualTo("body".toByteArray()) }
}

@Test
fun multipartContent_matchesWriteTo() {
val body =
multipartFormData(
jsonMapper(),
mapOf(
"field" to
MultipartField.builder<String>()
.value("value")
.contentType("text/plain")
.build(),
"binary" to
MultipartField.builder<ByteArray>()
.value("abc".toByteArray())
.contentType("application/octet-stream")
.build(),
),
)

val output = ByteArrayOutputStream()
body.writeTo(output)

body.content().use { content -> assertThat(content.readBytes()).isEqualTo(output.toByteArray()) }
}

@Test
fun multipartContent_streamsInputStreamParts() {
val body =
multipartFormData(
jsonMapper(),
mapOf(
"data" to
MultipartField.builder<java.io.InputStream>()
.value("stream content".byteInputStream().buffered())
.contentType("application/octet-stream")
.build()
),
)

val content = body.content().use { it.readBytes().toString(Charsets.UTF_8) }

assertThat(body.repeatable()).isFalse()
assertThat(content).contains("Content-Disposition: form-data; name=\"data\"")
assertThat(content).contains("Content-Type: application/octet-stream")
assertThat(content).contains("stream content")
}
}