Open fiadliel opened 6 years ago
We ran into a case, most likely caused by this issue, where when we try to decode a large (49,084 characters) response we get an exception:
java.lang.RuntimeException: org.http4s.InvalidBodyException: Received premature EOF.
Function looks like this:
override def getOrdersByOrderId(_req: Request[IO], organization: com.hbc.mobile.common.models.v0.models.Organization, orderId: Long, billingZipCode: String): IO[GetOrdersByOrderIdResponse] = {
val response: IO[GetOrdersByOrderIdResponse] = client.accounts.getOrdersByOrderId(orderId, billingZipCode, getSessionId(_req)).map { accountOrderTrackingResponseWrapper =>
GetOrdersByOrderIdResponse.HTTP200(accountOrderTrackingResponseWrapper.response.results.order.map(toAccountOrder).getOrElse {
throw new RuntimeException("No order present")
})
}
response.recoverWith {
case wrapperResponse: client.errors.AccountOrderStatusResponseWrapperResponse => {
wrapperResponse.response.status.code match {
case 404 => {
wrapperResponse.accountOrderStatusResponseWrapper.attempt.map {
case Left(e) =>
throw new RuntimeException(e)
case Right(accountOrderStatusResponseWrapper) =>
GetOrdersByOrderIdResponse.HTTP422(ValidationErrors(cleanupValidationErrors(accountOrderStatusResponseWrapper.errors.map(toValidationError))))
}
}
case undefined =>
throw new RuntimeException(s"undefined response code ${undefined}")
}
}
}
}
And we are getting a 404 back but we are not able to decode the response.
@amarrella If you could look at it tomorrow, that would be great, it's blocking some of our work.
The best thing would be to return a fs2.Stream in all the functions, but that's a pretty big breaking change.
Second best thing would be to do something like this in _executeRequest
:
httpClient
.streaming(modifyRequest(authBody))(response => fs2.Stream.emit(handler(response)))
.compile.foldMonoid.flatten
but this would add the requirement for F[U]
to be a monoid (even if we build the proof in the generated code, this means that we need to require U
to be a monoid at least). Again, this would be a breaking change.
I think the only option we have for now is replacing the fetch call with this:
for {
disposableResponse <- httpClient.open(request)
handledResponse <- handler(disposableResponse.response)
_ <- Sync[F].delay(disposableResponse.dispose)
} yield handledResponse
@fiadliel what do you think?
OK, bad news @mchimirev. After talking with @fiadliel we came to the conclusion that client is broken in this case and the only way to fix it is a pretty big breaking change (the ones suggested above won't solve the issue). My suggestion for now is to unfortunately not use the client, do the client manually and use the apibuilder ModelsJson.
@gheine @fiadliel any suggestion on how to move forward with this?
So the problem is confined to non-unit error responses and their lazily parsing the response body. A quick fix could be to make the error type non-lazy, ie. deserialize when the error response case class is constructed, similar to what happens when non-error response types are deserialized.
ie change from lazy val
to val
in:
case class ValidationErrorsResponse(
response: org.http4s.Response[F],
message: Option[String] = None
) extends Exception(message.getOrElse(response.status.code + ": " + response.body)){
lazy val validationErrors = _root_.com.foo.v0.Client.parseJson[F, com.foo.v0.models.ValidationErrors]("com.foo.v0.models.ValidationErrors", response)
}
Tested using mobile-middleware
(see branch issue-391-fix
), and every thing is working correctly. I especially love not having to .attempt.
response from the client.
For example, this was old code to handle 409:
response.recoverWith {
case wrapperResponse: client.errors.UserMessageResponse => {
wrapperResponse.response.status.code match {
case 409 => {
wrapperResponse.userMessage.attempt.map {
case Left(e) =>
throw new RuntimeException(e)
case Right(userMessage) =>
GetResponse.HTTP409(toUserMessage(userMessage))
}
}
case undefined =>
throw new RuntimeException(s"undefined response code ${undefined}")
}
}
}
And this is new code:
response.recover {
case wrapperResponse: client.errors.UserMessageResponse => {
wrapperResponse.response.status.code match {
case 409 => {
GetResponse.HTTP409(toUserMessage(wrapperResponse.userMessage))
}
case undefined =>
throw new RuntimeException(s"undefined response code ${undefined}")
}
}
}
_executeRequest
is defined like this:and uses the
fetch
method of the http4s client:If we look at the scaladoc for
fetch
:This means you should not access the body after this point in the code.
However, when an expected error occurs, the response is given back, including a call which tries to parse JSON in a lazy val. This is fundamentally unsafe for two reasons:
fetch
block).