dali
dali

Reputation: 63

Quarkus Client: Deserialization fails for Server Side Events

I trying to learn Quarkus, so feel free to correct me if I made any mistakes.

So now to the problem.

I have a server that sends SSE responses. I want to write a quarkus client to make calls to that server endpoint.
The problem is that, even though I have mapped correctly the response to a java record the quarkus app fails with error: ERROR [io.qua.ver.cor.run.VertxCoreRecorder] (vert.x-eventloop-thread-1) Uncaught exception received by Vert.x: jakarta.ws.rs.ProcessingException: Response could not be mapped to type class TestDTO for response with media type null

The actual server response is this:

< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/event-stream
< Cache-Control: no-cache
< Connection: keep-open
< Transfer-Encoding: chunked
< 
event: success
data: {"a":"str_value_1","date":"1970-01-01T02:00:00.000Z","b":1}

event: success
data: {"a":"str_value_2","date":"1970-01-01T02:00:00.000Z","b":2}

The quarkus aplication

  1. quarkus client interface
@Path("/api")
@RegisterRestClient(configKey = "service-api")
public interface TestClient {
    @GET
    @Path("test")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    Multi<TestDTO> getTest();
}
  1. TestDTO
@JsonIgnoreProperties(ignoreUnknown = true)
public record TestDTO(String a, LocalDate date, int b) {
    private static final ObjectWriter WRITER = new ObjectMapper()
            .writerWithDefaultPrettyPrinter()
            .withoutAttribute("jacksonObjectMapper");
    
    @Override
    public String toString() {
        try {
            return WRITER.writeValueAsString(this);
        } catch (Exception e) {
            return String.format("IssueComment[id=%s]", id);
        }
    }
}
  1. TestResource:
@Path("/test")
@ApplicationScoped
public class testResource {
    @RestClient
    GitHubServiceClient client;
    
    @GET
    @Produces(MediaType.SERVER_SENT_EVENTS)
    public Multi<TestDTO> hello() {
        return client.getTest().onItem().invoke(item -> System.out.println(item));
    }
}

An insight into the error

Now when I chenge thre TestDTO to TestDTO(String a) the client doesn't throw mapping error. the result I get might indicate where is the problem:

{
  "a" : "{\"a\":\"str_value_1\",\"date\":\"1970-01-01T02:00:00.000Z\",\"b\":1}"
}
{
  "a" : "{\"a\":\"str_value_2\",\"date\":\"1970-01-01T02:00:00.000Z\",\"b\":2}"
}

It seems that the obeject mapper instead of trying to map the data to the record, it interprets the SSE data as a string.


Workaround

One way I found to bypass this error is doing the mapping process manually. I change the client interface method to return Multi(ie Multi<String> getTest()), and use com.fasterxml.jackson.databind.ObjectMapper; in TestResource like this:

@Path("/test")
@ApplicationScoped
public class TestResource{
    @RestClient
    GitHubServiceClient client;

    @GET
    @Produces(MediaType.SERVER_SENT_EVENTS)
    public Multi<TestDTO> hello() {
        ObjectMapper objectMapper = new ObjectMapper();
        return client.getTest()
                     .map(Unchecked.function(s -> {
                         try {
                             return objectMapper.readValue(s, TestDTO.class);
                         } catch (JsonProcessingException e) {
                             throw new RuntimeException(e);
                         }
                     })).onItem().invoke(item -> System.out.println(item));
    }
}

Environment

Upvotes: 0

Views: 116

Answers (1)

geoand
geoand

Reputation: 64059

I created a simplified version of your example and everything works as expected

DTO:

public record Greet(String message) {
}

Client:

@RegisterRestClient(baseUri = "http://localhost:8080/sse")
public interface Client {

    @GET
    @RestStreamElementType(MediaType.APPLICATION_JSON)
    Multi<Greet> greet();

}

Endpoint returning the actual data:

@Path("/sse")
public class SseResource {

    @GET
    @RestStreamElementType(MediaType.APPLICATION_JSON)
    public Multi<Greet> greet() {
        return Multi.createFrom().items(new Greet("Hello"), new Greet("World"));
    }
}

Endpoint that calls the REST Client:

@Path("/client")
public class ClientResource {
    @RestClient
    Client client;

    @GET
    @RestStreamElementType(MediaType.APPLICATION_JSON)
    public Multi<Greet> greet() {
        return client.greet()
                .onItem().invoke(i -> System.out.println(i));
    }
}

Upvotes: 1

Related Questions