This is an automated email from the ASF dual-hosted git repository. afs pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit ad20bb4bba38dc7b1fa80b8168b3ff2bc974dd86 Author: Andy Seaborne <[email protected]> AuthorDate: Sun Apr 26 16:26:28 2026 +0100 Refactor HttpException; split into system errors and network response errors --- .../org/apache/jena/atlas/web/HttpException.java | 138 +++++++++++++++++++-- .../java/org/apache/jena/http/AsyncHttpRDF.java | 11 +- .../main/java/org/apache/jena/http/HttpLib.java | 17 +-- .../org/apache/jena/http/auth/AuthCredentials.java | 2 +- .../java/org/apache/jena/http/auth/AuthLib.java | 4 +- .../java/org/apache/jena/http/auth/DigestLib.java | 2 +- .../jena/sparql/exec/http/StoreProtocol.java | 2 +- .../fuseki/main/TestFusekiCustomOperation.java | 2 +- .../java/org/apache/jena/fuseki/test/HttpTest.java | 2 +- 9 files changed, 148 insertions(+), 32 deletions(-) diff --git a/jena-arq/src/main/java/org/apache/jena/atlas/web/HttpException.java b/jena-arq/src/main/java/org/apache/jena/atlas/web/HttpException.java index 98165d52f7..7128972cd7 100644 --- a/jena-arq/src/main/java/org/apache/jena/atlas/web/HttpException.java +++ b/jena-arq/src/main/java/org/apache/jena/atlas/web/HttpException.java @@ -21,6 +21,9 @@ package org.apache.jena.atlas.web; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; + import org.apache.jena.web.HttpSC; /** @@ -29,26 +32,122 @@ import org.apache.jena.web.HttpSC; public class HttpException extends RuntimeException { private final int statusCode; private final String statusLine; - private final String response; + // The body of the response, if any. + private final HttpHeaders responseHeaders; + private final String responseBody; + + /** System error setting up the HTTP request */ + public static HttpException error(String exceptionMessage) { + return new HttpException(exceptionMessage); + } + + /** System error setting up the HTTP request */ + public static HttpException error(String exceptionMessage, Throwable cause) { + return new HttpException(exceptionMessage, cause); + } + + /** HTTP error */ + public static HttpException create(int httpStatusCode) { + return HttpException.builder() + .statusCode(httpStatusCode) + .build(); + } + + /** HTTP error */ + public static HttpException create(HttpResponse<?> response) { + return HttpException.builder() + .statusCode(response.statusCode()) + .httpHeaders(response.headers()) + .build(); + } + + /** + * Replicate the details of an {@code HttpException}; + * the stacktrace will be the callers location. + */ + public static HttpException create(HttpException other) { + return HttpException.builder() + .statusCode(other.getStatusCode()) + .statusLine(other.getStatusLine()) + .responseMessage(other.getResponse()) + .cause(other.getCause()) + .build(); + } + + public static Builder builder() { + return new HttpException.Builder(); + } + + public static class Builder { + private int statusCode = -1; + private String statusLine = null; + private String responseMessage = null; + private Throwable cause = null; + private HttpHeaders httpHeaders = null; + + public Builder() {} + + public Builder statusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public Builder statusLine(String statusLine) { + this.statusLine = statusLine; + return this; + } + + public Builder responseMessage(String responseMessage) { + this.responseMessage = responseMessage; + return this; + } + + public Builder cause(Throwable cause) { + this.cause = cause; + return this; + } + + public Builder httpHeaders(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + public HttpException build() { + return new HttpException(statusCode, statusLine, responseMessage, cause); + } + } // HTTP/2 does not have an information message. + + /** @deprecated Use {@link HttpException#create(int)} */ + @Deprecated public HttpException(int statusCode) { - this(statusCode, null); + this(statusCode, null, null, null, null); } + /** @deprecated Use {@link HttpException#builder()} */ + @Deprecated public HttpException(int statusCode, String statusLine) { - this(statusCode, statusLine, null, null); + this(statusCode, statusLine, null, null, null); } + /** @deprecated Use {@link HttpException#create(HttpResponse)} or {@link HttpException#builder()} */ + @Deprecated public HttpException(int statusCode, String statusLine, String responseMessage) { - this(statusCode, statusLine, responseMessage, null); + this(statusCode, statusLine, null, responseMessage, null); } + /** @deprecated Use {@link HttpException#create(HttpResponse)} or {@link HttpException#builder()} */ + @Deprecated public HttpException(int statusCode, String statusLine, String responseMessage, Throwable cause) { + this(statusCode, statusLine, null, responseMessage, cause); + } + + private HttpException(int statusCode, String statusLine, HttpHeaders responseHttpHeaders, String responseBody, Throwable cause) { super(exMessage(statusCode, statusLine), cause); this.statusCode = statusCode; this.statusLine = statusLine ; - this.response = responseMessage; + this.responseHeaders = responseHttpHeaders; + this.responseBody = responseBody; } private static String exMessage(int statusCode, String statusLine) { @@ -57,25 +156,34 @@ public class HttpException extends RuntimeException { return statusCode+" - "+HttpSC.getMessage(statusCode); } - public HttpException(String message) { + /** @deprecated Use {@link HttpException#error(String)} */ + @Deprecated + private HttpException(String message) { super(message); this.statusCode = -1; this.statusLine = null ; - this.response = null; + this.responseHeaders = null; + this.responseBody = null; } - public HttpException(String message, Throwable cause) { + /** @deprecated Use {@link HttpException#error(String, Throwable)} */ + @Deprecated + private HttpException(String message, Throwable cause) { super(message, cause); this.statusCode = -1; this.statusLine = null ; - this.response = null; + this.responseHeaders = null; + this.responseBody = null; } + /** @deprecated Use {@link HttpException#builder()} */ + @Deprecated public HttpException(Throwable cause) { super(cause); this.statusCode = -1; this.statusLine = null ; - this.response = null; + this.responseHeaders = null; + this.responseBody = null; } /** @@ -100,6 +208,14 @@ public class HttpException extends RuntimeException { * @return The payload, or null if no payload */ public String getResponse() { - return response; + return responseBody; } + + /** + * The response headers. + */ + public HttpHeaders getHttpResponseHeader() { + return responseHeaders; + } + } diff --git a/jena-arq/src/main/java/org/apache/jena/http/AsyncHttpRDF.java b/jena-arq/src/main/java/org/apache/jena/http/AsyncHttpRDF.java index 41bf90efa5..5b4b367c41 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/AsyncHttpRDF.java +++ b/jena-arq/src/main/java/org/apache/jena/http/AsyncHttpRDF.java @@ -182,10 +182,9 @@ public class AsyncHttpRDF { } catch (CompletionException ex) { Throwable cause = ex.getCause(); if ( cause != null ) { - // Pass on our own HttpException instances such as 401 Unauthorized. if ( cause instanceof HttpException httpEx ) { - throw new HttpException(httpEx.getStatusCode(), httpEx.getStatusLine(), httpEx.getResponse(), cause); + throw HttpException.create(httpEx); } final String msg = cause.getMessage(); @@ -195,17 +194,17 @@ public class AsyncHttpRDF { if ( msg != null && ( msg.contains("too many authentication attempts") || msg.contains("No credentials provided") ) ) { - throw new HttpException(401, HttpSC.getMessage(401), null, cause); + throw HttpException.builder().statusCode(HttpSC.UNAUTHORIZED_401).cause(cause).build(); } if (httpRequest != null) { - throw new HttpException(httpRequest.method()+" "+httpRequest.uri().toString(), cause); + throw HttpException.error(httpRequest.method()+" "+httpRequest.uri().toString(), cause); } } - throw new HttpException(msg, cause); + throw HttpException.error(msg, cause); } // Note: CompletionException without cause should never happen. - throw new HttpException(ex); + throw HttpException.builder().cause(ex).build(); } } diff --git a/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java b/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java index 7394d97252..a2ffceb621 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java +++ b/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java @@ -62,7 +62,6 @@ import org.apache.jena.query.ARQ; import org.apache.jena.riot.web.HttpNames; import org.apache.jena.sparql.exec.http.Params; import org.apache.jena.sparql.util.Context; -import org.apache.jena.web.HttpSC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,7 +96,9 @@ public class HttpLib { InputStream in = r.body(); String msg = IO.readWholeFileAsUTF8(in); return msg; - } catch (Throwable ex) { throw new HttpException(ex); } + } catch (Throwable ex) { + throw HttpException.builder().cause(ex).build(); + } }; /** @@ -183,7 +184,7 @@ public class HttpLib { int httpStatusCode = response.statusCode(); // There is no status message in HTTP/2. if ( ! inRange(httpStatusCode, 100, 599) ) { - throw new HttpException("Status code out of range: "+httpStatusCode); + throw HttpException.error("Status code out of range: "+httpStatusCode); } if ( inRange(httpStatusCode, 100, 199) ) { // Informational @@ -269,7 +270,7 @@ public class HttpLib { try { return IO.readWholeFileAsUTF8(input); } catch (RuntimeIOException e) { - throw new HttpException(e); + throw HttpException.builder().cause(e).build(); } finally { finishInputStream(input); } @@ -282,7 +283,7 @@ public class HttpLib { static HttpException exception(HttpResponse<InputStream> response, int httpStatusCode) { InputStream in = response.body(); if ( in == null ) - return new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode)); + return HttpException.create(httpStatusCode); try { String msg; try { @@ -292,7 +293,7 @@ public class HttpLib { } catch (RuntimeIOException e) { msg = null; } - return new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode), msg); + return HttpException.builder().statusCode(httpStatusCode).responseMessage(msg).build(); } finally { IO.close(in); } } @@ -360,14 +361,14 @@ public class HttpLib { try { URI uri = new URI(uriStr); if ( ! uri.isAbsolute() ) - throw new HttpException("Not an absolute URL: <"+uriStr+">"); + throw HttpException.error("Not an absolute URL: <"+uriStr+">"); return uri; } catch (URISyntaxException ex) { int idx = ex.getIndex(); String msg = (idx<0) ? String.format("Bad URL: %s", uriStr) : String.format("Bad URL: %s starting at character %d", uriStr, idx); - throw new HttpException(msg, ex); + throw HttpException.error(msg, ex); } } diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthCredentials.java b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthCredentials.java index c776133684..d84ffef525 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthCredentials.java +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthCredentials.java @@ -43,7 +43,7 @@ public class AuthCredentials { // Checks. URI uri = location.getURI(); if ( uri.getRawQuery() != null || uri.getRawFragment() != null ) - throw new HttpException("Endpoint URI must not have query string or fragment: "+uri); + throw HttpException.error("Endpoint URI must not have query string or fragment: "+uri); authRegistry.put(location, pwRecord); prefixes.add(uri.toString(), location); } diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java index 5a96a79158..029a9870dd 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java @@ -101,7 +101,7 @@ public class AuthLib { passwordRecord = AuthEnv.get().getUsernamePassword(request.uri()); if ( passwordRecord == null ) // No entry. - throw new HttpException(HttpSC.UNAUTHORIZED_401); + throw HttpException.create(HttpSC.UNAUTHORIZED_401); } // Request target - no query string. @@ -126,7 +126,7 @@ public class AuthLib { // Not handled. Pass back the 401. return CompletableFuture.completedFuture(httpResponse401); default: - throw new HttpException("Not an authentication scheme -- "+aHeader.authScheme); + throw HttpException.error("Not an authentication scheme -- "+aHeader.authScheme); } // Failed to generate a request modifier for a retry. diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java b/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java index b20f27b720..654ce8e6ea 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java @@ -77,7 +77,7 @@ class DigestLib { private static Pair<String, String> getUserNameAndPassword(HttpClient httpClient) { Optional<Authenticator> optAuth = httpClient.authenticator(); if ( optAuth.isEmpty() ) - throw new HttpException("Username/password required but not present in HttpClient"); + throw HttpException.error("Username/password required but not present in HttpClient"); // We just want the PasswordAuthentication! PasswordAuthentication x = optAuth.orElseThrow().requestPasswordAuthenticationInstance(null, null, diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/StoreProtocol.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/StoreProtocol.java index 94276d01f5..ed46fd6c65 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/StoreProtocol.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/StoreProtocol.java @@ -183,7 +183,7 @@ public abstract class StoreProtocol<X extends StoreProtocol<X>> { // Setup problems. protected static RuntimeException exception(String msg) { - return new HttpException(msg); + return HttpException.error(msg); } final protected String service() { return serviceEndpoint; } diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java index b0f570d5a9..c27338e061 100644 --- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java @@ -248,7 +248,7 @@ public class TestFusekiCustomOperation { // Service endpoint name : GET String s1 = HttpOp.httpGetString(svcCall); if ( s1 == null ) - throw new HttpException(HttpSC.NOT_FOUND_404, "Not Found"); + throw HttpException.create(HttpSC.NOT_FOUND_404); assertValidResponseBody(customHandlerBodyGet, s1); // Service endpoint name : POST diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/HttpTest.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/HttpTest.java index 4e5a96fe27..ff6715013f 100644 --- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/HttpTest.java +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/HttpTest.java @@ -98,7 +98,7 @@ public class HttpTest { public static void expectQuery(Runnable action, int expected) { try { action.run(); - throw new HttpException("Expected QueryExceptionHTTP["+expected+"]"); + throw HttpException.error("Expected QueryExceptionHTTP["+expected+"]"); } catch (QueryExceptionHTTP ex) { if ( ex.getStatusCode() != expected ) throw ex;
