mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[boschshc] Handle invalid long poll responses gracefully (#16002)
If the long poll response from the Smart Home Controller does not contain valid JSON, the subscription is gracefully terminated a new one is initiated after 15 seconds. closes #15912 Signed-off-by: David Pace <dev@davidpace.de> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
5da654d9ca
commit
7e184b2152
@ -34,6 +34,8 @@ import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the long polling to the Smart Home Controller.
|
* Handles the long polling to the Smart Home Controller.
|
||||||
*
|
*
|
||||||
@ -141,7 +143,8 @@ public class LongPolling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start long polling the home controller. Once a long poll resolves, a new one is started.
|
* Start long polling the home controller. Once a long poll resolves, a new one
|
||||||
|
* is started.
|
||||||
*/
|
*/
|
||||||
private void longPoll(BoschHttpClient httpClient, String subscriptionId) {
|
private void longPoll(BoschHttpClient httpClient, String subscriptionId) {
|
||||||
logger.debug("Sending long poll request");
|
logger.debug("Sending long poll request");
|
||||||
@ -150,8 +153,9 @@ public class LongPolling {
|
|||||||
String url = httpClient.getBoschShcUrl("remote/json-rpc");
|
String url = httpClient.getBoschShcUrl("remote/json-rpc");
|
||||||
Request longPollRequest = httpClient.createRequest(url, POST, requestContent);
|
Request longPollRequest = httpClient.createRequest(url, POST, requestContent);
|
||||||
|
|
||||||
// Long polling responds after 20 seconds with an empty response if no update has happened.
|
// Long polling responds after 20 seconds with an empty response if no update
|
||||||
// 10 second threshold was added to not time out if response from controller takes a bit longer than 20 seconds.
|
// has happened. 10 second threshold was added to not time out if response
|
||||||
|
// from controller takes a bit longer than 20 seconds.
|
||||||
longPollRequest.timeout(30, TimeUnit.SECONDS);
|
longPollRequest.timeout(30, TimeUnit.SECONDS);
|
||||||
|
|
||||||
this.request = longPollRequest;
|
this.request = longPollRequest;
|
||||||
@ -159,8 +163,9 @@ public class LongPolling {
|
|||||||
longPollRequest.send(new BufferingResponseListener() {
|
longPollRequest.send(new BufferingResponseListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onComplete(@Nullable Result result) {
|
public void onComplete(@Nullable Result result) {
|
||||||
// NOTE: This handler runs inside the HTTP thread, so we schedule the response handling in a new thread
|
// NOTE: This handler runs inside the HTTP thread, so we schedule the response
|
||||||
// because the HTTP thread is terminated after the timeout expires.
|
// handling in a new thread because the HTTP thread is terminated after the
|
||||||
|
// timeout expires.
|
||||||
scheduler.execute(() -> longPolling.onLongPollComplete(httpClient, subscriptionId, result,
|
scheduler.execute(() -> longPolling.onLongPollComplete(httpClient, subscriptionId, result,
|
||||||
this.getContentAsString()));
|
this.getContentAsString()));
|
||||||
}
|
}
|
||||||
@ -188,10 +193,28 @@ public class LongPolling {
|
|||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
handleLongPollFailure(subscriptionId, failure);
|
handleLongPollFailure(subscriptionId, failure);
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Long poll response: {}", content);
|
handleLongPollResponse(httpClient, subscriptionId, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String nextSubscriptionId = subscriptionId;
|
/**
|
||||||
|
* Attempts to parse and process the long poll response content.
|
||||||
|
* <p>
|
||||||
|
* If the response cannot be parsed as {@link LongPollResult}, an attempt is made to parse a {@link LongPollError}.
|
||||||
|
* In case a {@link LongPollError} is present with the code <code>SUBSCRIPTION_INVALID</code>, a re-subscription is
|
||||||
|
* initiated.
|
||||||
|
* <p>
|
||||||
|
* If the response does not contain syntactically valid JSON, a new subscription is attempted with a delay of 15
|
||||||
|
* seconds.
|
||||||
|
*
|
||||||
|
* @param httpClient HTTP client which received the response
|
||||||
|
* @param subscriptionId Id of subscription the response is for
|
||||||
|
* @param content Content of the response
|
||||||
|
*/
|
||||||
|
private void handleLongPollResponse(BoschHttpClient httpClient, String subscriptionId, String content) {
|
||||||
|
logger.debug("Long poll response: {}", content);
|
||||||
|
|
||||||
|
try {
|
||||||
LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, LongPollResult.class);
|
LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, LongPollResult.class);
|
||||||
if (longPollResult != null && longPollResult.result != null) {
|
if (longPollResult != null && longPollResult.result != null) {
|
||||||
this.handleResult.accept(longPollResult);
|
this.handleResult.accept(longPollResult);
|
||||||
@ -212,10 +235,14 @@ public class LongPolling {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
// Execute next run
|
this.handleFailure.accept(
|
||||||
this.longPoll(httpClient, nextSubscriptionId);
|
new LongPollingFailedException("Could not deserialize long poll response: '" + content + "'", e));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute next run
|
||||||
|
this.longPoll(httpClient, subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLongPollFailure(String subscriptionId, Throwable failure) {
|
private void handleLongPollFailure(String subscriptionId, Throwable failure) {
|
||||||
|
@ -55,6 +55,7 @@ import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
|
|||||||
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
|
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link LongPolling}.
|
* Unit tests for {@link LongPolling}.
|
||||||
@ -405,6 +406,50 @@ class LongPollingTest {
|
|||||||
bufferingResponseListener.onComplete(result);
|
bufferingResponseListener.onComplete(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests a case in which the Smart Home Controller returns a HTML error response that is not parsable as JSON.
|
||||||
|
* <p>
|
||||||
|
* See <a href="https://github.com/openhab/openhab-addons/issues/15912">Issue 15912</a>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void startLongPollingInvalidLongPollResponse()
|
||||||
|
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
|
||||||
|
when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
|
||||||
|
|
||||||
|
Request subscribeRequest = mock(Request.class);
|
||||||
|
when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
|
||||||
|
argThat((JsonRpcRequest r) -> "RE/subscribe".equals(r.method)))).thenReturn(subscribeRequest);
|
||||||
|
SubscribeResult subscribeResult = new SubscribeResult();
|
||||||
|
when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
|
||||||
|
|
||||||
|
Request longPollRequest = mock(Request.class);
|
||||||
|
when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
|
||||||
|
argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
|
||||||
|
|
||||||
|
fixture.start(httpClient);
|
||||||
|
|
||||||
|
ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
|
||||||
|
verify(longPollRequest).send(completeListener.capture());
|
||||||
|
|
||||||
|
BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
|
||||||
|
|
||||||
|
String longPollResultContent = "<HTML><HEAD><TITLE>400</TITLE></HEAD><BODY><H1>400 Unsupported HTTP Protocol Version: /remote/json-rpcHTTP/1.1</H1></BODY></HTML>";
|
||||||
|
Response response = mock(Response.class);
|
||||||
|
bufferingResponseListener.onContent(response,
|
||||||
|
ByteBuffer.wrap(longPollResultContent.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
|
||||||
|
Result result = mock(Result.class);
|
||||||
|
bufferingResponseListener.onComplete(result);
|
||||||
|
|
||||||
|
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
|
||||||
|
verify(failureHandler).accept(throwableCaptor.capture());
|
||||||
|
Throwable t = throwableCaptor.getValue();
|
||||||
|
assertEquals(
|
||||||
|
"Could not deserialize long poll response: '<HTML><HEAD><TITLE>400</TITLE></HEAD><BODY><H1>400 Unsupported HTTP Protocol Version: /remote/json-rpcHTTP/1.1</H1></BODY></HTML>'",
|
||||||
|
t.getMessage());
|
||||||
|
assertTrue(t.getCause() instanceof JsonSyntaxException);
|
||||||
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void afterEach() {
|
void afterEach() {
|
||||||
fixture.stop();
|
fixture.stop();
|
||||||
|
Loading…
Reference in New Issue
Block a user