I want to pass messages to bus via REST, and get it back. But I cant correctly setup the message bus receiver, it throws java.lang.IllegalStateException: Response has already been written. In real life message bus should receive messages from different sources and pass a message to another target. Therefore we just need to publish the message to the bus. But how to correctly read messages and handle all of them? For example from a REST interface: read that messages!
My simple app start:
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new RESTVerticle());
vertx.deployVerticle(new Receiver());
EventBus eventBus = vertx.eventBus();
eventBus.registerDefaultCodec(MessageDTO.class, new CustomMessageCodec());
}
REST part
public class RESTVerticle extends AbstractVerticle {
private EventBus eventBus = null;
#Override
public void start() throws Exception {
Router router = Router.router(vertx);
eventBus = vertx.eventBus();
router.route().handler(BodyHandler.create());
router.route().handler(CorsHandler.create("*")
.allowedMethod(HttpMethod.GET)
.allowedHeader("Content-Type"));
router.post("/api/message").handler(this::publishToEventBus);
// router.get("/api/messagelist").handler(this::getMessagesFromBus);
router.route("/*").handler(StaticHandler.create());
vertx.createHttpServer().requestHandler(router::accept).listen(9999);
System.out.println("Service running at 0.0.0.0:9999");
}
private void publishToEventBus(RoutingContext routingContext) {
System.out.println("routingContext.getBodyAsString() " + routingContext.getBodyAsString());
final MessageDTO message = Json.decodeValue(routingContext.getBodyAsString(),
MessageDTO.class);
HttpServerResponse response = routingContext.response();
response.setStatusCode(201)
.putHeader("content-type", "application/json; charset=utf-8")
.end(Json.encodePrettily(message));
eventBus.publish("messagesBus", message);
}
And the Receiver: I move it to a different class, but it does not help
public class Receiver extends AbstractVerticle {
#Override
public void start() throws Exception {
EventBus eventBus = vertx.eventBus();
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
router.route().handler(CorsHandler.create("*")
.allowedMethod(HttpMethod.GET)
.allowedHeader("Content-Type"));
router.get("/api/messagelist").handler(this::getMessagesFromBus);
router.route("/*").handler(StaticHandler.create());
vertx.createHttpServer().requestHandler(router::accept).listen(9998);
System.out.println("Service Receiver running at 0.0.0.0:9998");
private void getMessagesFromBus(RoutingContext routingContext) {
EventBus eventBus = vertx.eventBus();
eventBus.consumer("messagesBus", message -> {
MessageDTO customMessage = (MessageDTO) message.body();
HttpServerResponse response = routingContext.response();
System.out.println("Receiver ->>>>>>>> " + customMessage);
if (customMessage != null) {
response.putHeader("content-type", "application/json; charset=utf-8")
.end(Json.encodePrettily(customMessage));
}
response.closed();
});
}
So if i post message to REST and handler publish it to the bus, when I am runtime get http://localhost:9998/api/messagelist it is return json, but second time it trow exception
java.lang.IllegalStateException: Response has already been written
at io.vertx.core.http.impl.HttpServerResponseImpl.checkWritten(HttpServerResponseImpl.java:561)
at io.vertx.core.http.impl.HttpServerResponseImpl.putHeader(HttpServerResponseImpl.java:154)
at io.vertx.core.http.impl.HttpServerResponseImpl.putHeader(HttpServerResponseImpl.java:52)
at com.project.backend.Receiver.lambda$getMessagesFromBus$0(Receiver.java:55)
at io.vertx.core.eventbus.impl.HandlerRegistration.handleMessage(HandlerRegistration.java:207)
at io.vertx.core.eventbus.impl.HandlerRegistration.handle(HandlerRegistration.java:201)
at io.vertx.core.eventbus.impl.EventBusImpl.lambda$deliverToHandler$127(EventBusImpl.java:498)
at io.vertx.core.impl.ContextImpl.lambda$wrapTask$18(ContextImpl.java:335)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:358)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:357)
at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:112)
at java.lang.Thread.run(Thread.java:745)
Receiver ->>>>>>>> Message{username=Aaaewfewf2d, message=41414wefwef2d2}
How to correctly get all messages from the receiver? Or if the bus received messages, should I immediately store them to the db? Can a message bus keep messages and not lost them?
Thanks
Each hit in the entry point "/api/messagelist" creates one new consumer with the request routing context.
The first request will create the consumer and reply to the request. When the second message was published, that consumer will receive the message and will reply to the previous request (instance) and this was closed.
I think that you misunderstood the event bus purpose and I really recommend you to read the documentation.
http://vertx.io/docs/vertx-core/java/#event_bus
I did not had the chance to test your code but it seems that the publish operation is throwing an exception and vertx will try to send back an error message. However you already replied and ended the connection.
Now the error might be from your codec but due to the asynchronous nature of vertx you only see it at a later stage and mangled with the internal error handler.
Related
I'm new to Spring Boot and web applications. I have to send notifications of unhandled/unread messages from a Spring Boot backend to a web client. I decided to use Server Sent Events since I think I don't need a bidirectional connection (otherwise I'd have thought of WebSockets).
I made a very simple REST controller which finds all unhandled messages in a db and sends them to the client. The problem is that it keeps sending forever all the messages, while I'd like to send a message only when it is added to the db, or when the client first connects to the server.
The behaviour I'd like to achieve is similar to a mail client or a messaging app, in which the user is notifyed not only on new messages but also of previous ones if he/she didn't mark them as read. The notification should happen only once when the client connects, not loop forever.
Here is my code:
#RestController
#CrossOrigin(origins = "*")
public class SseEmitterController {
private MessageDAO messageDAO;
private ExecutorService nonBlockingService = Executors
.newCachedThreadPool();
#Autowired
public SseEmitterController(MessageDAO messageDAO) {
this.messageDAO = messageDAO;
}
#GetMapping("/incoming_messages")
public SseEmitter handleSse() {
SseEmitter emitter = new SseEmitter();
nonBlockingService.execute(() -> {
try {
List<Message> messages = messageDAO.findByHandledFalse();
for (Message message: messages) {
emitter.send(message, MediaType.APPLICATION_JSON);
}
emitter.complete();
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
}
I know that the problem is caused by the fact that I query the db inside handleSse method, but I couldn't figure out how to do it outside.
Could you please help me?
Update October 05, 2021
I found out how to solve the problem, I didn't update the question because I didn't have the time, but since someone asked me to do so in the comments, I'm gonna explain my solution, hoping it may be helpful.
Here's my code:
The SseEmitterController is responsible for invoking the SseService on frontend's request:
#RestController
#CrossOrigin(origins = "*")
public class SseEmitterController {
private final SseService sseService;
#Autowired
SseEmitterController(SseService sseService) {
this.sseService = sseService;
}
#GetMapping("/incoming_messages")
public ResponseEntity<SseEmitter> handleSse() {
final SseEmitter emitter = new SseEmitter();
sseService.addEmitter(emitter);
emitter.onCompletion(() -> sseService.removeEmitter(emitter));
emitter.onTimeout(() -> sseService.removeEmitter(emitter));
return new ResponseEntity<>(emitter, HttpStatus.OK);
}
}
The SseService is called on a new message arrival (from another part of the application) and sends the notification (actually a server sent event) to the frontend (which previously called the endpoint in the controller above.
The service is called like so: sseService.sendHelpRequestNotification(helpRequest);
#Service
public class SseService {
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
public void addEmitter(final SseEmitter emitter) {
emitters.add(emitter);
}
public void removeEmitter(final SseEmitter emitter) {
emitters.remove(emitter);
}
public void sendMessagesNotification(Message message) {
List<SseEmitter> sseEmitterListToRemove = new ArrayList<>();
emitters.forEach((SseEmitter emitter) -> {
try {
emitter.send(message, MediaType.APPLICATION_JSON);
} catch (Exception e) {
sseEmitterListToRemove.add(emitter);
}
});
emitters.removeAll(sseEmitterListToRemove);
}
}
And finally there is another controller to get all previous unhandled messages (not involving server sent events):
#GetMapping(value = "/unhandled_help_requests")
public ResponseEntity<List<HelpRequest>> getUnhandledMessages() {
List<Message> resultSet = messageDAO.findByHandledFalse(Sort.by("date").and(Sort.by("time")));
return new ResponseEntity<>(resultSet, HttpStatus.OK);
}
So, to sum it up: the frontend calls the SseEmitterController to listen for new SSEs. These SSEs are created and sent whenever a new message arrives to the backend, via the SseService. Finally, to get all unhandled (for whatever reason) messages, there is a specific old fashioned controller.
I'm setting up a api gateway. I want to verrify Authorization token before request BE service. I got IllegalStateException: Request has already been read. Please help.
I uploaded the test project code to GitHub.
https://github.com/EddyPan/test-demo
router.route().path("/user/admin").method(HttpMethod.POST)
.handler(rct -> {
HttpServerRequest request = rct.request().setExpectMultipart(true);
MultiMap headers = request.headers();
JsonObject param = new JsonObject().put("requestUrl", "http://localhost:18080/authorize")
.put("httpMethod", "POST");
webClient.postAbs("http://localhost:18080/authorize")
.timeout(6000)
.putHeader("Content-Type", "application/json")
.putHeader("Authorization", headers.get("Authorization"))
.as(BodyCodec.jsonObject())
.sendJsonObject(param, ar -> authHandler(rct, ar));
});
exception:
java.lang.IllegalStateException: Request has already been read
at io.vertx.core.http.impl.HttpServerRequestImpl.checkEnded(HttpServerRequestImpl.java:599)
at io.vertx.core.http.impl.HttpServerRequestImpl.setExpectMultipart(HttpServerRequestImpl.java:431)
at io.vertx.ext.web.impl.HttpServerRequestWrapper.setExpectMultipart(HttpServerRequestWrapper.java:208)
at com.demo.HttpServerVerticle.lambda$start$8(HttpServerVerticle.java:62)
at io.vertx.ext.web.impl.RouteImpl.handleContext(RouteImpl.java:232)
at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:121)
at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:134)
at com.demo.HttpServerVerticle.authHandler(HttpServerVerticle.java:132)
at com.demo.HttpServerVerticle.lambda$null$0(HttpServerVerticle.java:53)
at io.vertx.ext.web.client.impl.HttpContext.handleDispatchResponse(HttpContext.java:285)
at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:272)
at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:250)
at io.vertx.ext.web.client.impl.predicate.PredicateInterceptor.handle(PredicateInterceptor.java:69)
at io.vertx.ext.web.client.impl.predicate.PredicateInterceptor.handle(PredicateInterceptor.java:32)
at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:247)
at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:257)
at io.vertx.ext.web.client.impl.HttpContext.dispatchResponse(HttpContext.java:218)
at io.vertx.ext.web.client.impl.HttpContext.lambda$null$2(HttpContext.java:341)
at io.vertx.core.impl.ContextImpl.executeTask(ContextImpl.java:320)
at io.vertx.core.impl.EventLoopContext.lambda$executeAsync$0(EventLoopContext.java:38)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:163)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:404)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:495)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
I fixed this issue. Before call auth api I pause the original request, and resume buffer processing when the authorize finished.
router.route().path("/user/admin").method(HttpMethod.POST)
.handler(rct -> {
HttpServerRequest request = rct.request().setExpectMultipart(true);
request.pause(); // Here is to pasue the origin request.
MultiMap headers = request.headers();
JsonObject param = new JsonObject().put("requestUrl", "http://localhost:18080/authorize")
.put("httpMethod", "POST");
webClient.postAbs("http://localhost:18080/authorize")
.timeout(6000)
.putHeader("Content-Type", "application/json")
.putHeader("Authorization", headers.get("Authorization"))
.as(BodyCodec.jsonObject())
.sendJsonObject(param, ar -> authHandler(rct, ar));
});
Two reasons you might end up with this error:
A) async Handler before BodyHandler
As stated in the Vert.X documentation for BodyHandler:
The usage of this handler requires that it is installed as soon as possible in the router since it needs to install handlers to consume the HTTP request body and this must be done before executing any async call.
Fix 1: Change the order:
router.post(endpoint)
.consumes(contentType)
.handler(bodyHandler) <<<<<<<<< first this
.handler(authHandler) <<<<<<<< then this async handler;
Fix 2: pause/resume request delivery:
See Vert.X documentation:
If an async call is required before, the HttpServerRequest should be paused and then resumed so that the request events are not delivered until the body handler is ready to process them.
router.post(endpoint)
.consumes(contentType)
.handler(authHandler)
.handler(bodyHandler);
BodyHandler implements Handler<RoutingContext> {
#Override
public void handle(final RoutingContext ctx) {
// pause request delivery
ctx.request().pause();
asyncCall(r -> {
// e.g. check authorization or database here
// resume request delivery
ctx.request.resume();
// call the next handler
ctx.next();
}
}
}
B) Multiple request.body() calls
Suppost you use the Vert.X BodyHandler and have a custom handler installed after it:
router.post(endpoint)
.consumes(contentType)
.handler(BodyHandler.create())
.handler(customHandler);
Your custom handler must not call request.body()! or else you get
403: body has already been read
Fix: Use ctx.getBody()
Use ctx.getBody[/asJson/asString]() to get the body already read by the BodyHandler:
CustomHandler implements Handler<RoutingContext> {
#Override
public void handleAuthorizedRequest(RoutingContext ctx) {
final var body = ctx.getBodyAsJson();
// instead of: ctx.request().body();
...
}
}
I want to read the messages received by a websocket in onmessage event. I follow the explanation made in this thread
I only achieved to add a Listener and be informed when the websocket is created but I donĀ“t know how to read the messages received by the websocket in onmessage event.
Here is my code:
public class TestBet365Socket {
public static void main(String[] args) throws Exception {
/* turn off annoying htmlunit warnings */
java.util.logging.Logger.getLogger("com.gargoylesoftware").setLevel(java.util.logging.Level.OFF);
WebClient client = new WebClient(BrowserVersion.CHROME);
client.getOptions().setCssEnabled(false);
client.getOptions().setJavaScriptEnabled(true);
client.getOptions().setThrowExceptionOnScriptError(false);
client.getInternals().addListener(new Listener() {
#Override
public void webSocketCreated(WebSocket arg0) {
System.out.println("Websocket Created " + arg0);
}
});
HtmlPage page = client.getPage("https://mobile.bet365.com/Default.aspx?lng=3");
client.waitForBackgroundJavaScript(10000);
List<NameValuePair> response =page.getWebResponse().getResponseHeaders();
for (NameValuePair header : response) {
System.out.println(header.getName() + " = " + header.getValue());
}
System.out.println(page.asText());
client.close();
}
}
You have to do two things:
implement you own listener
Create a class that implements org.eclipse.jetty.websocket.api.WebSocketListener. Inside this class you can implement you way of capturing the socket communication.
set the listener
The method webSocketCreated() gots a WebSocket instance as parameter. Call setWebSocketListener(yourListener) with the listener you have created in the first step.
Now you are done and your listener will be called every time the websocket gots data.
I'm facing the following problem and I found no working solution yet.
I have 3 different applications that should communicate with each other:
the UI part (1)
the backend application (2)
the microservice "in the cloud" (3)
The backend application provides a Webservice (REST) for the UI to get and put information from/to the microservice.
Everything I want to grab from the microservice works fine, but:
If I want to put data to the microservice, the specs require a websocket connection. This works fine too, but the microservice returns a message after the (un-)successful command, like
{"statusCode":200,"messageId":"1234567890"}
The problem now is: How can I grab this message in my application and send it back to the UI, so the user knows if the command was successful?
For the moment I tried this:
WebSocketClient.java
#OnMessage
public void onMessage(Session session, String msg) {
if (this.messageHandler != null) {
this.messageHandler.handleMessage(msg);
}
}
public void addMessageHandler(MessageHandler msgHandler) {
this.messageHandler = msgHandler;
}
public static interface MessageHandler {
public String handleMessage(String message);
}
MyTotalAwesomeController.java
public class MyTotalAwesomeController {
WebSocketClient wsc = new WebSocketClient();
...
#RequestMapping(value="/add", method={RequestMethod.POST, RequestMethod.OPTIONS})
public ResponseEntity<Object> putDataToMicroservice(#RequestBody Map<String, Object> payload, #RequestHeader(value = "authorization") String authorizationHeader) throws Exception {
...
wsc.addMessageHandler(new WebSocketClient.MessageHandler() {
public String handleMessage(String message) {
System.out.println("RETURN MSG FROM WSS : " + message);
return message;
}
});
return ResponseEntity.ok("worked");
}
I can see the console output from the MessageHandler return, but I don't know how I can pass this to the parent method for return insted of just returning the ResponseEntity.ok().
I'm not very used to WebSocket connections in Java yet, so please don't judge me ;-)
Thank you for your help.
The code below will work under the assumption that the #OnMessage method is executed in a thread managed by the WebSocket client runtime. Please inspect the thread that runs the #OnMessage method.
If the above premise is true, the putDataToMicroservice() method, executed by a thread in the global scope, will wait until the WebSocket response arrives at the WS client thread, which will repass the message to the global scope thread. Then the execution in your controller class will continue.
public class MyTotalAwesomeController {
WebSocketClient wsc = new WebSocketClient();
// Queue for communication between threads.
private BlockingQueue<String> queue;
#PostConstruct
void init() {
queue = new SynchronousQueue<>(true);
// This callback will be invoked by the WebSocket thread.
wsc.addMessageHandler(new WebSocketClient.MessageHandler() {
#Override
public String handleMessage(String message) {
System.out.println("RETURN MSG FROM WSS : " + message);
// Pass message to the controller thread.
queue.put(message);
// Note that the return value is not necessary.
// You can take it out of the interface as well.
return null;
}
});
}
#RequestMapping(value="/add", method={RequestMethod.POST, RequestMethod.OPTIONS})
public ResponseEntity<Object> putDataToMicroservice(#RequestBody Map<String, Object> payload, #RequestHeader(value = "authorization") String authorizationHeader) throws Exception {
// At this point you make a WebSocket request, is that right?
doWebSocketRequest();
// This poll call will block the current thread
// until the WebSocket server responds,
// or gives up waiting after the specified timeout.
//
// When the WebSocket server delivers a response,
// the WS client implementation will execute the
// #OnMessage annotated method in a thread
// managed by the WS client itself.
//
// The #OnMessage method will pass the message
// to this thread in the queue below.
String message = queue.poll(30, TimeUnit.SECONDS);
if (message == null) {
// WebSocket timeout.
}
return ResponseEntity.ok("worked");
}
}
I'm using Spring JMS and ActiveMQ to send message from a sender to multiple listeners using ActiveMQ Topic (publish/subscribe). So far all listeners can receive message from the sender. But I want to add a functionality that when a particular listener, say listener1, gets the message, listener1 will send a receipt confirmation to the sender. I followed the comment in my old post and created a TemporaryQueue in the sender and used ReplyTo in the sender and receiver to get the confirmation message from the listener to the sender.
My sender class is:
public class CustomerStatusSender {
private JmsTemplate jmsTemplate;
private Topic topic;
public void setJmsTemplate(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void setTopic(Topic topic) {
this.topic = topic;
}
public void simpleSend(final String customerStatusMessage) {
jmsTemplate.send(topic, new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
TextMessage message = session.createTextMessage("hello world");
message.setStringProperty("content", customerStatusMessage);
message.setIntProperty("count", 10);
//send acknowledge request to a listener via a tempQueue
Destination tempQueue = session.createTemporaryQueue();
message.setJMSCorrelationID("replyMessage");
message.setJMSReplyTo(tempQueue);
return message;
}
});
}
}
The sender creates a TemporaryQueue for the listener to send back the confirmation message. Then in one of the listeners, I have the following code to send the confirmation message back to the sender:
public class CustomerStatusListener implements SessionAwareMessageListener<Message> {
public void onMessage(Message message, Session session) {
if (message instanceof TextMessage) {
try {
System.out.println("Subscriber 1 got you! The message is: "
+ message.getStringProperty("content"));
//create a receipt confirmation message and send it back to the sender
Message response = session.createMessage();
response.setJMSCorrelationID(message.getJMSCorrelationID());
response.setBooleanProperty("Ack", true);
TemporaryQueue tempQueue = (TemporaryQueue) message.getJMSReplyTo();
MessageProducer producer = session.createProducer(tempQueue);
producer.send(tempQueue, response);
} catch (JMSException ex) {
throw new RuntimeException(ex);
}
} else {
throw new IllegalArgumentException(
"Message must be of type TextMessage");
}
}
}
However, I found that the following line in the Listener class throws an error:
TemporaryQueue tempQueue = (TemporaryQueue) message.getJMSReplyTo();
MessageProducer producer = session.createProducer(tempQueue);
The exception error says:
The destination temp-queue://ID:xyz-1385491-1:2:1 does not exist.
So what's wrong here? I assume that tempQueue created by the sender is available for the listener in the same JMS session. Why the tempQueue object after calling message.getJMSReplyTo() does not return a valid TemporaryQueue?
The other question is: How do I receive the confirmation message in the sender? Should I implements MessageListener interface in the sender in order to receive the confirmation from the listener? Or should I just call receive() method to receive it synchronously?
Thanks for any suggestions!
If you are using spring-jms, why not just use a MessageListenerAdapter as your listener? - he will take care of the replyTo stuff and your listener can be a simple POJO.
In any case, you don't need to cast it to a Tempoarary queue; it's just a destination as far as the listener is concerned.
Checkout the MessageListenerAdapter` javadocs.
Also, you need to create a consumer on the temp queue on the sending side, to receive the reply. If the sending connection is closed, the temp queue will go away.
I ended up using a separate JMS Queue to send the acknowledge message from the listener-1 to the sender. For some reason, the temporaryQueue created by ActiveMQ is not available during the JMS session.