[energidataservice] Consider Retry-After header (#20899)

* Consider Retry-After header

Resolves #20897

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen
2026-06-09 21:05:25 +02:00
committed by GitHub
parent a5a1598428
commit 88ac63a464
4 changed files with 99 additions and 12 deletions
@@ -14,6 +14,7 @@ package org.openhab.binding.energidataservice.internal;
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -53,6 +54,7 @@ import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecords
import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer;
import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
import org.openhab.binding.energidataservice.internal.exception.DataServiceRateLimitException;
import org.openhab.core.i18n.TimeZoneProvider;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
@@ -198,10 +200,34 @@ public class ApiController {
updatePropertiesFromResponse(response, properties);
int status = response.getStatus();
String responseContent = response.getContentAsString();
if (!HttpStatus.isSuccess(status)) {
if (logger.isTraceEnabled()) {
logger.trace("Request failed with HTTP error {}: {}", status, responseContent);
logger.trace("Response headers: {}", response.getHeaders());
}
if (status == HttpStatus.TOO_MANY_REQUESTS_429) {
String retryAfter = response.getHeaders().get("Retry-After");
if (retryAfter != null) {
try {
int retryAfterSeconds = Integer.parseInt(retryAfter);
if (retryAfterSeconds < 0) {
logger.debug("Invalid Retry-After header value: '{}'", retryAfter);
throw new DataServiceRateLimitException("Rate limit is exceeded");
}
throw new DataServiceRateLimitException(
"Rate limit is exceeded. Retrying after " + retryAfter + " seconds.",
Duration.ofSeconds(retryAfterSeconds));
} catch (NumberFormatException e) {
logger.debug("Invalid Retry-After header value: '{}'", retryAfter);
throw new DataServiceRateLimitException("Rate limit is exceeded", e);
}
}
}
throw new DataServiceException("The request failed with HTTP error " + status, status);
}
String responseContent = response.getContentAsString();
if (responseContent.isEmpty()) {
throw new DataServiceException("Empty response");
}
@@ -16,7 +16,6 @@ import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
@@ -161,7 +160,7 @@ public class EnergiDataServiceCommandExtension extends AbstractConsoleCommandExt
@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage(SUBCMD_UPDATE + " ["
return List.of(buildCommandUsage(SUBCMD_UPDATE + " ["
+ String.join("|", Stream.of(PriceComponent.values()).map(PriceComponent::toString).toList())
+ "] <StartDate> [<EndDate>]", "Update time series in requested period"));
}
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.energidataservice.internal.exception;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.http.HttpStatus;
/**
* {@link DataServiceRateLimitException} is a specialized Energi Data Service exception
* thrown in case of exceeding the API rate limit. It contains information about how long to
* wait before retrying the request.
*
* @see <a href="https://www.energidataservice.dk/guides/api-guides">Energi Data Service API documentation on rate
* limiting</a>
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class DataServiceRateLimitException extends DataServiceException {
private static final Duration DEFAULT_RETRY_AFTER = Duration.ofMinutes(30);
private static final long serialVersionUID = 1L;
private final Duration retryAfter;
public DataServiceRateLimitException(String message) {
this(message, DEFAULT_RETRY_AFTER);
}
public DataServiceRateLimitException(String message, Throwable cause) {
super(message, HttpStatus.TOO_MANY_REQUESTS_429);
initCause(cause);
this.retryAfter = DEFAULT_RETRY_AFTER;
}
public DataServiceRateLimitException(String message, Duration retryAfter) {
super(message, HttpStatus.TOO_MANY_REQUESTS_429);
this.retryAfter = retryAfter;
}
public Duration getRetryAfter() {
return retryAfter;
}
}
@@ -20,6 +20,7 @@ import java.time.ZoneId;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
import org.openhab.binding.energidataservice.internal.exception.DataServiceRateLimitException;
import org.openhab.binding.energidataservice.internal.retry.strategy.ExponentialBackoff;
import org.openhab.binding.energidataservice.internal.retry.strategy.FixedTime;
import org.openhab.binding.energidataservice.internal.retry.strategy.Linear;
@@ -40,16 +41,20 @@ public class RetryPolicyFactory {
* @return retry strategy
*/
public static RetryStrategy fromThrowable(Throwable e) {
if (e instanceof DataServiceException dse) {
switch (dse.getHttpStatus()) {
case HttpStatus.TOO_MANY_REQUESTS_429:
return new ExponentialBackoff().withMinimum(Duration.ofMinutes(30));
default:
return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2);
}
}
ExponentialBackoff strategy = switch (e) {
case DataServiceRateLimitException rle ->
new ExponentialBackoff()
.withMinimum(rle.getRetryAfter());
return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2);
case DataServiceException dse when dse.getHttpStatus() == HttpStatus.TOO_MANY_REQUESTS_429 ->
new ExponentialBackoff()
.withMinimum(Duration.ofMinutes(30));
default -> new ExponentialBackoff()
.withMinimum(Duration.ofMinutes(1));
};
return strategy.withJitter(0.2);
}
/**