diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java index fc6ae008fc7..7924702d6be 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java @@ -34,6 +34,8 @@ import org.openhab.binding.boschshc.internal.serialization.GsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonSyntaxException; + /** * 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) { logger.debug("Sending long poll request"); @@ -150,8 +153,9 @@ public class LongPolling { String url = httpClient.getBoschShcUrl("remote/json-rpc"); Request longPollRequest = httpClient.createRequest(url, POST, requestContent); - // Long polling responds after 20 seconds with an empty response if no update has happened. - // 10 second threshold was added to not time out if response from controller takes a bit longer than 20 seconds. + // Long polling responds after 20 seconds with an empty response if no update + // 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); this.request = longPollRequest; @@ -159,8 +163,9 @@ public class LongPolling { longPollRequest.send(new BufferingResponseListener() { @Override public void onComplete(@Nullable Result result) { - // NOTE: This handler runs inside the HTTP thread, so we schedule the response handling in a new thread - // because the HTTP thread is terminated after the timeout expires. + // NOTE: This handler runs inside the HTTP thread, so we schedule the response + // handling in a new thread because the HTTP thread is terminated after the + // timeout expires. scheduler.execute(() -> longPolling.onLongPollComplete(httpClient, subscriptionId, result, this.getContentAsString())); } @@ -188,10 +193,28 @@ public class LongPolling { if (failure != null) { handleLongPollFailure(subscriptionId, failure); } else { - logger.debug("Long poll response: {}", content); + handleLongPollResponse(httpClient, subscriptionId, content); + } + } - String nextSubscriptionId = subscriptionId; + /** + * Attempts to parse and process the long poll response content. + *
+ * 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 SUBSCRIPTION_INVALID
, a re-subscription is
+ * initiated.
+ *
+ * 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); if (longPollResult != null && longPollResult.result != null) { this.handleResult.accept(longPollResult); @@ -212,10 +235,14 @@ public class LongPolling { } } } - - // Execute next run - this.longPoll(httpClient, nextSubscriptionId); + } catch (JsonSyntaxException e) { + this.handleFailure.accept( + 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) { diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java index 4db50f71c3c..7b4ea728883 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java @@ -55,6 +55,7 @@ import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; /** * Unit tests for {@link LongPolling}. @@ -405,6 +406,50 @@ class LongPollingTest { bufferingResponseListener.onComplete(result); } + /** + * Tests a case in which the Smart Home Controller returns a HTML error response that is not parsable as JSON. + *
+ * See Issue 15912
+ */
+ @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);
+
+ ArgumentCaptor400 Unsupported HTTP Protocol Version: /remote/json-rpcHTTP/1.1
";
+ Response response = mock(Response.class);
+ bufferingResponseListener.onContent(response,
+ ByteBuffer.wrap(longPollResultContent.getBytes(StandardCharsets.UTF_8)));
+
+ Result result = mock(Result.class);
+ bufferingResponseListener.onComplete(result);
+
+ ArgumentCaptor400 Unsupported HTTP Protocol Version: /remote/json-rpcHTTP/1.1
'",
+ t.getMessage());
+ assertTrue(t.getCause() instanceof JsonSyntaxException);
+ }
+
@AfterEach
void afterEach() {
fixture.stop();