import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.gson.JsonSyntaxException;
+
/**
* Handles the long polling to the Smart Home Controller.
*
}
/**
- * 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");
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;
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()));
}
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.
+ * <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);
if (longPollResult != null && longPollResult.result != null) {
this.handleResult.accept(longPollResult);
}
}
}
-
- // 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) {
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
/**
* Unit tests for {@link LongPolling}.
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
void afterEach() {
fixture.stop();