http4s generator: generated code results in very awkward error handling
Closed this issue · 4 comments
For example, see saksdirect/mobile-middleware project, AccountService.scala
Let's say we have an endpoint like this:
"method": "POST",
"path": "/accounts",
"description": "Sign the user up. (Register new account).",
"body": {
"type": "account_sign_up_request_data",
"required": true
},
"parameters": [
{
"name": "sessionId",
"type": "uuid",
"location": "header"
}
],
"responses": {
"200": {
"type": "account_sign_up_response_wrapper"
},
"400": {
"type": "account_sign_up_errors_wrapper"
},
"409": {
"type": "account_sign_up_errors_wrapper"
}
}
},
Then generated code looks like this:
case r if r.status.code == 200 => _root_.com.hbc.mobile.saks.mobile.middleware.api.v1.Client.parseJson[F, com.hbc.mobile.saks.mobile.middleware.api.v1.models.AccountSignUpResponseWrapper]("com.hbc.mobile.saks.mobile.middleware.api.v1.models.AccountSignUpResponseWrapper", r)
case r if r.status.code == 400 => Sync[F].raiseError(new com.hbc.mobile.saks.mobile.middleware.api.v1.errors.AccountSignUpErrorsWrapperResponse(r))
case r if r.status.code == 409 => Sync[F].raiseError(new com.hbc.mobile.saks.mobile.middleware.api.v1.errors.AccountSignUpErrorsWrapperResponse(r))
case r => Sync[F].raiseError(new com.hbc.mobile.saks.mobile.middleware.api.v1.errors.FailedRequest(r.status.code, s"Unsupported response code[${r.status.code}]. Expected: 200, 400, 409"))
}
This results in very awkward client code (to handle 400 and 409):
val response: IO[PostRegisterResponse] = client.accounts.postAccounts(toAccountSignUpRequestData(form), getSessionId(_req)).map { wrapper =>
PostRegisterResponse.HTTP200(toAccount(wrapper))
}
response.recoverWith {
case wrapperResponse: sakserrors.AccountSignUpErrorsWrapperResponse[IO] => {
wrapperResponse.response.status.code match {
case 400 | 409 => {
wrapperResponse.accountSignUpErrorsWrapper.attempt.map(_.toOption).map(accountSignUpErrorsWrapper => {
PostRegisterResponse.HTTP422(ValidationErrors(cleanupValidationErrors(accountSignUpErrorsWrapper.get.errors.map(toValidationError))))
})
}
case undefined =>
throw new RuntimeException(s"undefined response code ${undefined}")
}
}
}
- response has to be typed, just doing .recoverWith is not enough
- doing .attempt. seems wrong
- AccountSignUpErrorsWrapperResponse has to be typed with [IO], this results in a compilation warning, because we cannot really guarantee the type at runtime
@fiadliel Can you add / correct my comment?
Hello @mchimirev , i tried to minimize the use case for testing (I don't have access to the repo you are referring to).
Here is the api.json I tested:
{
"name": "test",
"models": {
"okModel": {
"fields": [
{
"name": "name",
"type": "string"
}
]
},
"errorModel": {
"fields": [
{
"name": "description",
"type": "string"
}
]
}
},
"resources": {
"okModel": {
"path": "/test",
"operations": [
{
"method": "GET",
"path": "/",
"responses": {
"200": {
"type":"okModel"
},
"422": {
"type":"errorModel"
},
"404": {
"type":"errorModel"
}
}
}
]
}
}
}
The generated *Client.scala
is here: https://gist.github.com/amarrella/93b18bbe407da365f2efc56002375992)
It seems to me that you can handle your use case this way:
package com.example
import cats.effect.IO
import com.example.test.v0.Client
import com.example.test.v0.errors.ErrorModelResponse
import org.http4s.Uri
import org.http4s.client.blaze._
object Test extends App {
for {
httpClient <- Http1Client[IO]()
apiClient = new Client[IO](Uri.unsafeFromString("https://www.example.com"), httpClient = httpClient)
response <- apiClient.okModels.get().attempt
} yield {
response match {
case Right(x) => x
case Left(e: ErrorModelResponse[_]) =>
e.response.status.code match {
case 422 => e.errorModel // Do something with it
case 404 => e.errorModel // Do something with it
}
case Left(_) => throw new Exception("Unhandled")
}
}
}
Notice that:
attempt
is called immediately after theget
- Pattern matching in the errors ignores the type of ErrorModelResponse (so no compiler warnings) but I can still find out the error and do something with it.
This works because ErrorModelResponse[_]
extends Exception
.
Did I understand the problem correctly? Does this solve it?
My point (in discussion with @mchimirev) was that ErrorModelResponse
always has the same F
as the client, but this is not encoded in the API. I suspect that the response error should be defined inside the client, so that it inherits the same F
.
What is currently there can work, but it's inelegant, considering what we know about the invariants.
Alternatively, the error response could be altered so that it doesn't need an F
. This is a questionable design anyway, in my opinion (e.g. how sure are we about the timescale under which this F
can be evaluated).
Ok I agree with that, we can move the error responses into the client class in the generated code. I'll open a PR