Skip to content

Commit 14b016c

Browse files
caaladortepi
andauthored
feat: resend payload of reoccuring client request (#20547)
* feat: resend payload of reoccuring client request If the client request contains the previous id and exactly the same message content respond with the same payload as previously. Closes #20506 * Rename exception, remove initial value. --------- Co-authored-by: Teppo Kurki <[email protected]>
1 parent 380b15f commit 14b016c

File tree

5 files changed

+144
-15
lines changed

5 files changed

+144
-15
lines changed

flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java

+21
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ public List<Object> getParameters() {
215215

216216
private byte[] lastProcessedMessageHash = null;
217217

218+
private String lastRequestResponse;
219+
218220
private String contextRootRelativePath;
219221

220222
private String appId;
@@ -305,6 +307,25 @@ public void setLastProcessedClientToServerId(
305307
this.lastProcessedMessageHash = lastProcessedMessageHash;
306308
}
307309

310+
/**
311+
* Sets the response created for the last UIDL request.
312+
*
313+
* @param lastRequestResponse
314+
* The request that was sent for the last UIDL request.
315+
*/
316+
public void setLastRequestResponse(String lastRequestResponse) {
317+
this.lastRequestResponse = lastRequestResponse;
318+
}
319+
320+
/**
321+
* Returns the response created for the last UIDL request.
322+
*
323+
* @return The request that was sent for the last UIDL request.
324+
*/
325+
public String getLastRequestResponse() {
326+
return lastRequestResponse;
327+
}
328+
308329
/**
309330
* Gets the server sync id.
310331
* <p>

flow-server/src/main/java/com/vaadin/flow/server/communication/ServerRpcHandler.java

+18-3
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,19 @@ public ResynchronizationRequiredException() {
236236
}
237237
}
238238

239+
/**
240+
* Exception thrown when the client side re-sends the same request.
241+
*/
242+
public static class ClientResentPayloadException extends RuntimeException {
243+
244+
/**
245+
* Default constructor for the exception.
246+
*/
247+
public ClientResentPayloadException() {
248+
super();
249+
}
250+
}
251+
239252
/**
240253
* Reads JSON containing zero or more serialized RPC calls (including legacy
241254
* variable changes) and executes the calls.
@@ -317,9 +330,11 @@ public void handleRpc(UI ui, String message, VaadinRequest request)
317330
* situation is most likely triggered by a timeout or such
318331
* causing a message to be resent.
319332
*/
320-
getLogger().info(
321-
"Ignoring old duplicate message from the client. Expected: "
322-
+ expectedId + ", got: " + requestId);
333+
getLogger().debug(
334+
"Received old duplicate message from the client. Expected: "
335+
+ expectedId + ", got: " + requestId
336+
+ ". Resending previous response.");
337+
throw new ClientResentPayloadException();
323338
} else if (rpcRequest.isUnloadBeaconRequest()) {
324339
getLogger().debug(
325340
"Ignoring unexpected message id from the client on UNLOAD request. "

flow-server/src/main/java/com/vaadin/flow/server/communication/UidlRequestHandler.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.vaadin.flow.server.VaadinService;
4040
import com.vaadin.flow.server.VaadinSession;
4141
import com.vaadin.flow.server.communication.ServerRpcHandler.InvalidUIDLSecurityKeyException;
42+
import com.vaadin.flow.server.communication.ServerRpcHandler.ClientResentPayloadException;
4243
import com.vaadin.flow.server.communication.ServerRpcHandler.ResynchronizationRequiredException;
4344
import com.vaadin.flow.server.dau.DAUUtils;
4445
import com.vaadin.flow.server.dau.DauEnforcementException;
@@ -134,8 +135,10 @@ public Optional<ResponseWriter> synchronizedHandleRequest(
134135
StringWriter stringWriter = new StringWriter();
135136

136137
try {
137-
getRpcHandler(session).handleRpc(uI, requestBody, request);
138+
getRpcHandler().handleRpc(uI, requestBody, request);
138139
writeUidl(uI, stringWriter, false);
140+
} catch (ClientResentPayloadException e) {
141+
stringWriter.write(uI.getInternals().getLastRequestResponse());
139142
} catch (JsonException e) {
140143
getLogger().error("Error writing JSON to response", e);
141144
// Refresh on client side
@@ -176,6 +179,7 @@ void writeUidl(UI ui, Writer writer, boolean resync) throws IOException {
176179

177180
// some dirt to prevent cross site scripting
178181
String responseString = "for(;;);[" + uidl.toJson() + "]";
182+
ui.getInternals().setLastRequestResponse(responseString);
179183
writer.write(responseString);
180184
}
181185

@@ -208,7 +212,7 @@ public boolean handleSessionExpired(VaadinRequest request,
208212
return true;
209213
}
210214

211-
private ServerRpcHandler getRpcHandler(VaadinSession session) {
215+
private ServerRpcHandler getRpcHandler() {
212216
ServerRpcHandler handler = rpcHandler.get();
213217
if (handler == null) {
214218
rpcHandler.compareAndSet(null, createRpcHandler());

flow-server/src/test/java/com/vaadin/flow/server/communication/ServerRpcHandlerTest.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.vaadin.flow.server.communication;
22

33
import java.io.IOException;
4-
import java.io.Reader;
54
import java.io.StringReader;
65

76
import org.junit.Assert;
@@ -99,9 +98,9 @@ public void handleRpc_resynchronize_throwsExceptionAndDirtiesTreeAndClearsDepend
9998
Mockito.verify(dependencyList).clearPendingSendToClient();
10099
}
101100

102-
@Test
103-
public void handleRpc_duplicateMessage_doNotThrow()
104-
throws InvalidUIDLSecurityKeyException, IOException {
101+
@Test(expected = ServerRpcHandler.ClientResentPayloadException.class)
102+
public void handleRpc_duplicateMessage_throwsResendPayload()
103+
throws InvalidUIDLSecurityKeyException {
105104
String msg = "{\"" + ApplicationConstants.CLIENT_TO_SERVER_ID + "\":1}";
106105
ServerRpcHandler handler = new ServerRpcHandler();
107106

flow-server/src/test/java/com/vaadin/flow/server/communication/UidlRequestHandlerTest.java

+96-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import java.io.IOException;
2121
import java.io.OutputStream;
22-
import java.io.Reader;
2322
import java.io.StringWriter;
2423
import java.util.Collections;
2524
import java.util.Optional;
@@ -31,6 +30,7 @@
3130
import org.mockito.Mockito;
3231

3332
import com.vaadin.flow.component.UI;
33+
import com.vaadin.flow.function.DeploymentConfiguration;
3434
import com.vaadin.flow.server.DefaultDeploymentConfiguration;
3535
import com.vaadin.flow.server.HandlerHelper.RequestType;
3636
import com.vaadin.flow.server.MockVaadinContext;
@@ -46,6 +46,7 @@
4646
import com.vaadin.flow.server.startup.ApplicationConfiguration;
4747
import com.vaadin.flow.shared.ApplicationConstants;
4848
import com.vaadin.pro.licensechecker.dau.EnforcementException;
49+
import com.vaadin.tests.util.MockUI;
4950

5051
import elemental.json.JsonObject;
5152
import elemental.json.impl.JsonUtil;
@@ -128,9 +129,73 @@ public void writeSessionExpired_whenUINotFound() throws IOException {
128129
responseContent);
129130
}
130131

132+
@Test
133+
public void clientRequestsPreviousIdAndPayload_resendPreviousResponse()
134+
throws IOException {
135+
136+
UI ui = getUi();
137+
VaadinSession session = ui.getSession();
138+
VaadinService service = session.getService();
139+
DeploymentConfiguration conf = Mockito
140+
.mock(DeploymentConfiguration.class);
141+
Mockito.when(service.getDeploymentConfiguration()).thenReturn(conf);
142+
Mockito.when(conf.isRequestTiming()).thenReturn(false);
143+
144+
String requestBody = """
145+
{
146+
"csrfToken": "d1f44a6f-bbe5-4493-a8a9-3f5f234a2a93",
147+
"rpc": [
148+
{
149+
"type": "mSync",
150+
"node": 12,
151+
"feature": 1,
152+
"property": "value",
153+
"value": "a"
154+
},
155+
{
156+
"type": "event",
157+
"node": 12,
158+
"event": "change",
159+
"data": {}
160+
}
161+
],
162+
"syncId": 0,
163+
"clientId": 0
164+
}
165+
""";
166+
Mockito.when(request.getService()).thenReturn(service);
167+
Mockito.when(conf.isSyncIdCheckEnabled()).thenReturn(true);
168+
169+
Optional<SynchronizedRequestHandler.ResponseWriter> result = handler
170+
.synchronizedHandleRequest(session, request, response,
171+
requestBody);
172+
Assert.assertTrue("ResponseWriter should be present",
173+
result.isPresent());
174+
result.get().writeResponse();
175+
String responseContent = CommunicationUtil
176+
.getStringWhenWriteString(outputStream);
177+
178+
// Init clean response
179+
response = Mockito.mock(VaadinResponse.class);
180+
outputStream = Mockito.mock(OutputStream.class);
181+
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
182+
183+
result = handler.synchronizedHandleRequest(session, request, response,
184+
requestBody);
185+
Assert.assertTrue("ResponseWriter should be present",
186+
result.isPresent());
187+
result.get().writeResponse();
188+
String resendResponseContent = CommunicationUtil
189+
.getStringWhenWriteString(outputStream);
190+
191+
// response shouldn't contain async
192+
Assert.assertEquals("Server should send same content again",
193+
responseContent, resendResponseContent);
194+
}
195+
131196
@Test
132197
public void should_modifyUidl_when_MPR() throws Exception {
133-
UI ui = mock(UI.class);
198+
UI ui = getUi();
134199

135200
UidlRequestHandler handler = spy(new UidlRequestHandler());
136201
StringWriter writer = new StringWriter();
@@ -151,7 +216,7 @@ public void should_modifyUidl_when_MPR() throws Exception {
151216

152217
@Test
153218
public void should_changeURL_when_v7LocationProvided() throws Exception {
154-
UI ui = mock(UI.class);
219+
UI ui = getUi();
155220

156221
UidlRequestHandler handler = spy(new UidlRequestHandler());
157222
StringWriter writer = new StringWriter();
@@ -172,7 +237,7 @@ public void should_changeURL_when_v7LocationProvided() throws Exception {
172237
@Test
173238
public void should_updateHash_when_v7LocationNotProvided()
174239
throws Exception {
175-
UI ui = mock(UI.class);
240+
UI ui = getUi();
176241

177242
UidlRequestHandler handler = spy(new UidlRequestHandler());
178243
StringWriter writer = new StringWriter();
@@ -192,7 +257,7 @@ public void should_updateHash_when_v7LocationNotProvided()
192257

193258
@Test
194259
public void should_not_modify_non_MPR_Uidl() throws Exception {
195-
UI ui = mock(UI.class);
260+
UI ui = getUi();
196261

197262
UidlRequestHandler handler = spy(new UidlRequestHandler());
198263
StringWriter writer = new StringWriter();
@@ -217,7 +282,7 @@ public void should_not_modify_non_MPR_Uidl() throws Exception {
217282
@Test
218283
public void should_not_update_browser_history_if_no_hash_in_location()
219284
throws Exception {
220-
UI ui = mock(UI.class);
285+
UI ui = getUi();
221286

222287
UidlRequestHandler handler = spy(new UidlRequestHandler());
223288
StringWriter writer = new StringWriter();
@@ -351,4 +416,29 @@ private JsonObject getUidlWithNoHashInLocation() {
351416
// @formatter:on
352417
}
353418

419+
/**
420+
* Mock ui with session.
421+
*
422+
* @return
423+
*/
424+
private static UI getUi() {
425+
VaadinService service = mock(VaadinService.class);
426+
VaadinSession session = new VaadinSession(service) {
427+
@Override
428+
public boolean hasLock() {
429+
return true;
430+
}
431+
432+
@Override
433+
public VaadinService getService() {
434+
return service;
435+
}
436+
};
437+
438+
UI ui = new MockUI(session);
439+
440+
when(service.findUI(Mockito.any())).thenReturn(ui);
441+
442+
return ui;
443+
}
354444
}

0 commit comments

Comments
 (0)