Streamline error handling

pull/30/head
Richard Körber 2016-12-21 23:28:44 +01:00
parent 4a2d7c4178
commit 32bfe32077
5 changed files with 72 additions and 47 deletions

View File

@ -50,6 +50,7 @@ import org.shredzone.acme4j.exception.AcmeRateLimitExceededException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.exception.AcmeUnauthorizedException;
import org.shredzone.acme4j.util.AcmeUtils;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
import org.slf4j.Logger;
@ -61,10 +62,6 @@ import org.slf4j.LoggerFactory;
public class DefaultConnection implements Connection {
private static final Logger LOG = LoggerFactory.getLogger(DefaultConnection.class);
public static final String ACME_ERROR_PREFIX = "urn:ietf:params:acme:error:";
private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:";
private static final String ACCEPT_HEADER = "Accept";
private static final String ACCEPT_CHARSET_HEADER = "Accept-Charset";
private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language";
@ -193,7 +190,12 @@ public class DefaultConnection implements Connection {
}
JSON json = readJsonResponse();
throw createAcmeException(rc, json);
if (rc == HttpURLConnection.HTTP_CONFLICT) {
throw new AcmeConflictException(json.get("detail").asString(), getLocation());
}
throw createAcmeException(json);
} catch (IOException ex) {
throw new AcmeNetworkException(ex);
}
@ -361,45 +363,35 @@ public class DefaultConnection implements Connection {
/**
* Handles a problem by throwing an exception. If a JSON problem was returned, an
* {@link AcmeServerException} will be thrown. Otherwise a generic
* {@link AcmeServerException} or subtype will be thrown. Otherwise a generic
* {@link AcmeException} is thrown.
*/
private AcmeException createAcmeException(int rc, JSON json) {
private AcmeException createAcmeException(JSON json) {
String type = json.get("type").asString();
String detail = json.get("detail").asString();
if (detail == null) {
detail = "general problem";
}
if (rc == HttpURLConnection.HTTP_CONFLICT) {
return new AcmeConflictException(detail, getLocation());
}
String error = AcmeUtils.stripErrorPrefix(type);
if (type == null) {
return new AcmeException(detail);
}
switch (type) {
case ACME_ERROR_PREFIX + "unauthorized":
case ACME_ERROR_PREFIX_DEPRECATED + "unauthorized":
return new AcmeUnauthorizedException(type, detail);
case ACME_ERROR_PREFIX + "agreementRequired":
case ACME_ERROR_PREFIX_DEPRECATED + "agreementRequired":
String instance = json.get("instance").asString();
return new AcmeAgreementRequiredException(
type, detail, getLink("terms-of-service"),
instance != null ? resolveRelative(instance) : null);
case ACME_ERROR_PREFIX + "rateLimited":
case ACME_ERROR_PREFIX_DEPRECATED + "rateLimited":
return new AcmeRateLimitExceededException(
type, detail, getRetryAfterHeader(), getLinks("rate-limit"));
default:
return new AcmeServerException(type, detail);
if ("unauthorized".equals(error)) {
return new AcmeUnauthorizedException(type, detail);
}
if ("agreementRequired".equals(error)) {
URI instance = resolveRelative(json.get("instance").asString());
URI tos = getLink("terms-of-service");
return new AcmeAgreementRequiredException(type, detail, tos, instance);
}
if ("rateLimited".equals(error)) {
Date retryAfter = getRetryAfterHeader();
Collection<URI> rateLimits = getLinks("rate-limit");
return new AcmeRateLimitExceededException(type, detail, retryAfter, rateLimits);
}
return new AcmeServerException(type, detail);
}
/**
@ -439,10 +431,16 @@ public class DefaultConnection implements Connection {
* Resolves a relative link against the connection's last URI.
*
* @param link
* Link to resolve. Absolute links are just converted to an URI.
* @return Absolute URI of the given link.
* Link to resolve. Absolute links are just converted to an URI. May be
* {@code null}.
* @return Absolute URI of the given link, or {@code null} if the link was
* {@code null}.
*/
private URI resolveRelative(String link) {
if (link == null) {
return null;
}
assertConnectionIsOpen();
try {
return new URL(conn.getURL(), link).toURI();

View File

@ -15,7 +15,7 @@ package org.shredzone.acme4j.exception;
import java.util.Objects;
import org.shredzone.acme4j.connector.DefaultConnection;
import org.shredzone.acme4j.util.AcmeUtils;
/**
* An exception that is thrown when the ACME server returned an error. It contains
@ -24,8 +24,6 @@ import org.shredzone.acme4j.connector.DefaultConnection;
public class AcmeServerException extends AcmeException {
private static final long serialVersionUID = 5971622508467042792L;
private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:";
private final String type;
/**
@ -57,13 +55,7 @@ public class AcmeServerException extends AcmeException {
* {@code "urn:ietf:params:acme:error"}
*/
public String getAcmeErrorType() {
if (type.startsWith(DefaultConnection.ACME_ERROR_PREFIX)) {
return type.substring(DefaultConnection.ACME_ERROR_PREFIX.length());
} else if (type.startsWith(ACME_ERROR_PREFIX_DEPRECATED)) {
return type.substring(ACME_ERROR_PREFIX_DEPRECATED.length());
} else {
return null;
}
return AcmeUtils.stripErrorPrefix(type);
}
}

View File

@ -40,6 +40,8 @@ import org.shredzone.acme4j.exception.AcmeProtocolException;
*/
public final class AcmeUtils {
private static final char[] HEX = "0123456789abcdef".toCharArray();
private static final String ACME_ERROR_PREFIX = "urn:ietf:params:acme:error:";
private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:";
private static final Pattern DATE_PATTERN = Pattern.compile(
"^(\\d{4})-(\\d{2})-(\\d{2})T"
@ -203,4 +205,26 @@ public final class AcmeUtils {
return cal.getTime();
}
/**
* Strips the acme error prefix from the error string.
* <p>
* For example, for "urn:ietf:params:acme:error:conflict", "conflict" is returned.
* <p>
* This method also handles the deprecated prefix "urn:acme:error:" that is still in
* use at Let's Encrypt.
*
* @param type
* Error type to strip the prefix from. {@code null} is safe.
* @return Stripped error type, or {@code null} if the prefix was not found.
*/
public static String stripErrorPrefix(String type) {
if (type != null && type.startsWith(ACME_ERROR_PREFIX)) {
return type.substring(ACME_ERROR_PREFIX.length());
} else if (type != null && type.startsWith(ACME_ERROR_PREFIX_DEPRECATED)) {
return type.substring(ACME_ERROR_PREFIX_DEPRECATED.length());
} else {
return null;
}
}
}

View File

@ -446,7 +446,7 @@ public class DefaultConnectionTest {
} catch (AcmeNetworkException ex) {
fail("Did not expect an AcmeNetworkException");
} catch (AcmeException ex) {
assertThat(ex.getMessage(), not(isEmptyOrNullString()));
assertThat(ex.getMessage(), isEmptyOrNullString());
}
verify(mockUrlConnection).getHeaderField("Content-Type");

View File

@ -233,6 +233,17 @@ public class AcmeUtilsTest {
}
}
/**
* Test that error prefix is correctly removed.
*/
@Test
public void testStripErrorPrefix() {
assertThat(stripErrorPrefix("urn:ietf:params:acme:error:unauthorized"), is("unauthorized"));
assertThat(stripErrorPrefix("urn:acme:error:deprecated"), is("deprecated"));
assertThat(stripErrorPrefix("urn:somethingelse:error:message"), is(nullValue()));
assertThat(stripErrorPrefix(null), is(nullValue()));
}
/**
* Matches the given time.
*/