Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ internal inline fun <reified T> jsonHandler(jsonMapper: JsonMapper): Handler<T>
try {
jsonMapper.readValue(response.body(), jacksonTypeRef())
} catch (e: Exception) {
throw OpenAIInvalidDataException("Error reading response", e)
throw OpenAIInvalidDataException("Error reading response", e, response.headers())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package com.openai.core.handlers

import com.openai.core.http.Headers
import com.openai.core.http.HttpResponse
import com.openai.core.http.HttpResponse.Handler
import com.openai.core.http.PhantomReachableClosingStreamResponse
Expand Down Expand Up @@ -30,7 +31,7 @@ internal fun <T> streamHandler(
// We wrap the `lines` instead of the top-level sequence because
// we only want to catch `IOException` from the reader; not from
// the user's own code.
IOExceptionWrappingSequence(lines),
IOExceptionWrappingSequence(lines, response.headers()),
)
}
}
Expand All @@ -53,7 +54,10 @@ internal fun <T> streamHandler(
}

/** A sequence that catches, wraps, and rethrows [IOException] as [OpenAIIoException]. */
private class IOExceptionWrappingSequence<T>(private val sequence: Sequence<T>) : Sequence<T> {
private class IOExceptionWrappingSequence<T>(
private val sequence: Sequence<T>,
private val headers: Headers,
) : Sequence<T> {

override fun iterator(): Iterator<T> {
val iterator = sequence.iterator()
Expand All @@ -63,14 +67,14 @@ private class IOExceptionWrappingSequence<T>(private val sequence: Sequence<T>)
try {
iterator.next()
} catch (e: IOException) {
throw OpenAIIoException("Stream failed", e)
throw OpenAIIoException("Stream failed", e, headers)
}

override fun hasNext(): Boolean =
try {
iterator.hasNext()
} catch (e: IOException) {
throw OpenAIIoException("Stream failed", e)
throw OpenAIIoException("Stream failed", e, headers)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
package com.openai.errors

import com.openai.core.http.Headers
import java.util.Optional

class OpenAIInvalidDataException
@JvmOverloads
constructor(message: String? = null, cause: Throwable? = null) : OpenAIException(message, cause)
constructor(message: String? = null, cause: Throwable? = null) : OpenAIException(message, cause) {

private var responseHeaders: Headers? = null

internal constructor(message: String?, cause: Throwable?, headers: Headers) : this(message, cause) {
responseHeaders = headers
}

fun headers(): Optional<Headers> = Optional.ofNullable(responseHeaders)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
package com.openai.errors

import com.openai.core.http.Headers
import java.util.Optional

class OpenAIIoException
@JvmOverloads
constructor(message: String? = null, cause: Throwable? = null) : OpenAIException(message, cause)
constructor(message: String? = null, cause: Throwable? = null) : OpenAIException(message, cause) {

private var responseHeaders: Headers? = null

internal constructor(message: String?, cause: Throwable?, headers: Headers) : this(message, cause) {
responseHeaders = headers
}

fun headers(): Optional<Headers> = Optional.ofNullable(responseHeaders)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.openai.core.handlers

import com.fasterxml.jackson.databind.json.JsonMapper
import com.openai.core.http.Headers
import com.openai.core.http.HttpResponse
import com.openai.errors.OpenAIInvalidDataException
import java.io.InputStream
import kotlin.test.Test
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.assertThrows

internal class JsonHandlerTest {

@Test
fun jsonHandler_whenBodyCannotBeRead_exposesResponseHeaders() {
val headers = Headers.builder().put("x-request-id", "req_123").build()
val handler = jsonHandler<Map<String, Any>>(JsonMapper.builder().build())

val error =
assertThrows<OpenAIInvalidDataException> {
handler.handle(httpResponse("{".byteInputStream(), headers))
}

assertThat(error).hasMessage("Error reading response")
assertThat(error.headers()).contains(headers)
}

private fun httpResponse(body: InputStream, headers: Headers): HttpResponse =
object : HttpResponse {

override fun statusCode(): Int = 200

override fun headers(): Headers = headers

override fun body(): InputStream = body

override fun close() {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ internal class StreamHandlerTest {

@Test
fun streamHandler_whenReaderThrowsIOException_wrapsException() {
val headers = Headers.builder().put("x-request-id", "req_123").build()
val handler = streamHandler<String> { _, lines -> lines.forEach {} }
val streamResponse = handler.handle(httpResponse("a\nb\nc\n".byteInputStream().throwing()))
val streamResponse =
handler.handle(httpResponse("a\nb\nc\n".byteInputStream().throwing(), headers))

val e = assertThrows<OpenAIIoException> { streamResponse.stream().forEach {} }
assertThat(e).hasMessage("Stream failed")
assertThat(e).hasCauseInstanceOf(IOException::class.java)
assertThat(e.headers()).contains(headers)
}

@Test
Expand All @@ -68,12 +71,15 @@ internal class StreamHandlerTest {
assertThat(e).isSameAs(ioException)
}

private fun httpResponse(body: InputStream): HttpResponse =
private fun httpResponse(
body: InputStream,
headers: Headers = Headers.builder().build(),
): HttpResponse =
object : HttpResponse {

override fun statusCode(): Int = 0

override fun headers(): Headers = Headers.builder().build()
override fun headers(): Headers = headers

override fun body(): InputStream = body

Expand Down