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;

Reply via email to