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 4ad0de7ae3890c504f206177c5a1dbcadda811cc
Author: Andy Seaborne <[email protected]>
AuthorDate: Sun Apr 26 20:31:37 2026 +0100

    GH-3679: Return HttpHeaders in HTTPException and QueryExceptionHTTP
---
 .../org/apache/jena/atlas/web/HttpException.java   |  49 ++++----
 .../main/java/org/apache/jena/http/HttpLib.java    |   8 +-
 .../java/org/apache/jena/http/auth/AuthLib.java    |   3 +-
 .../sparql/engine/http/QueryExceptionHTTP.java     | 132 ++++++++++++---------
 .../sparql/exec/http/TestQueryExecCleanServer.java |   7 +-
 .../jena/sparql/exec/http/TestQueryExecHTTP.java   |  25 ++++
 6 files changed, 137 insertions(+), 87 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 7128972cd7..d84da1c805 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
@@ -57,7 +57,7 @@ public class HttpException extends RuntimeException {
     public static HttpException create(HttpResponse<?> response) {
         return HttpException.builder()
                 .statusCode(response.statusCode())
-                .httpHeaders(response.headers())
+                .httpResponseHeaders(response.headers())
                 .build();
     }
 
@@ -69,6 +69,7 @@ public class HttpException extends RuntimeException {
         return HttpException.builder()
                 .statusCode(other.getStatusCode())
                 .statusLine(other.getStatusLine())
+                .httpResponseHeaders(other.getHttpResponseHeaders())
                 .responseMessage(other.getResponse())
                 .cause(other.getCause())
                 .build();
@@ -107,58 +108,58 @@ public class HttpException extends RuntimeException {
             return this;
         }
 
-        public Builder httpHeaders(HttpHeaders httpHeaders) {
+        public Builder httpResponseHeaders(HttpHeaders httpHeaders) {
             this.httpHeaders = httpHeaders;
             return this;
         }
         public HttpException build() {
-            return new HttpException(statusCode, statusLine, responseMessage, 
cause);
+            return new HttpException(statusCode, statusLine, httpHeaders, 
responseMessage, cause);
         }
     }
 
     // HTTP/2 does not have an information message.
 
+    private HttpException(int statusCode, String statusLine, HttpHeaders 
responseHttpHeaders, String responseBody, Throwable cause) {
+        super(exMessage(statusCode, statusLine), cause);
+        this.statusCode = statusCode;
+        this.statusLine = statusLine ;
+        this.responseHeaders = responseHttpHeaders;
+        this.responseBody = responseBody;
+    }
+
+    private static String exMessage(int statusCode, String statusLine) {
+        if ( statusLine == null )
+            statusLine = HttpSC.getMessage(statusCode);
+        return statusCode+" - "+HttpSC.getMessage(statusCode);
+    }
+
     /** @deprecated Use {@link HttpException#create(int)} */
-    @Deprecated
+    @Deprecated(forRemoval = true)
     public HttpException(int statusCode) {
         this(statusCode, null, null, null, null);
     }
 
     /** @deprecated Use {@link HttpException#builder()} */
-    @Deprecated
+    @Deprecated(forRemoval = true)
     public HttpException(int statusCode, String statusLine) {
         this(statusCode, statusLine, null, null, null);
     }
 
     /** @deprecated Use {@link HttpException#create(HttpResponse)} or {@link 
HttpException#builder()} */
-    @Deprecated
+    @Deprecated(forRemoval = true)
     public HttpException(int statusCode, String statusLine, String 
responseMessage) {
         this(statusCode, statusLine, null, responseMessage, null);
     }
 
     /** @deprecated Use {@link HttpException#create(HttpResponse)} or {@link 
HttpException#builder()} */
-    @Deprecated
+    @Deprecated(forRemoval = true)
     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.responseHeaders = responseHttpHeaders;
-        this.responseBody = responseBody;
-    }
-
-    private static String exMessage(int statusCode, String statusLine) {
-        if ( statusLine == null )
-            statusLine = HttpSC.getMessage(statusCode);
-        return statusCode+" - "+HttpSC.getMessage(statusCode);
-    }
-
     /** @deprecated Use {@link HttpException#error(String)} */
     @Deprecated
-    private HttpException(String message) {
+    public HttpException(String message) {
         super(message);
         this.statusCode = -1;
         this.statusLine = null ;
@@ -168,7 +169,7 @@ public class HttpException extends RuntimeException {
 
     /** @deprecated Use {@link HttpException#error(String, Throwable)} */
     @Deprecated
-    private HttpException(String message, Throwable cause) {
+    public HttpException(String message, Throwable cause) {
         super(message, cause);
         this.statusCode = -1;
         this.statusLine = null ;
@@ -214,7 +215,7 @@ public class HttpException extends RuntimeException {
     /**
      * The response headers.
      */
-    public HttpHeaders getHttpResponseHeader() {
+    public HttpHeaders getHttpResponseHeaders() {
         return responseHeaders;
     }
 
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 a2ffceb621..422978e154 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
@@ -283,7 +283,7 @@ public class HttpLib {
     static HttpException exception(HttpResponse<InputStream> response, int 
httpStatusCode) {
         InputStream in = response.body();
         if ( in == null )
-            return HttpException.create(httpStatusCode);
+            return HttpException.create(response);
         try {
             String msg;
             try {
@@ -293,7 +293,11 @@ public class HttpLib {
             } catch (RuntimeIOException e) {
                 msg = null;
             }
-            return 
HttpException.builder().statusCode(httpStatusCode).responseMessage(msg).build();
+            return HttpException.builder()
+                    .statusCode(httpStatusCode)
+                    .responseMessage(msg)
+                    .httpResponseHeaders(response.headers())
+                    .build();
         } finally { IO.close(in); }
     }
 
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 029a9870dd..14a2ac0b75 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
@@ -40,7 +40,6 @@ import org.apache.jena.atlas.web.HttpException;
 import org.apache.jena.http.AsyncHttpRDF;
 import org.apache.jena.http.HttpLib;
 import org.apache.jena.riot.web.HttpNames;
-import org.apache.jena.web.HttpSC;
 
 public class AuthLib {
     /**
@@ -101,7 +100,7 @@ public class AuthLib {
             passwordRecord = AuthEnv.get().getUsernamePassword(request.uri());
             if ( passwordRecord == null )
                 // No entry.
-                throw HttpException.create(HttpSC.UNAUTHORIZED_401);
+                throw HttpException.create(httpResponse401);
         }
 
         // Request target - no query string.
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/http/QueryExceptionHTTP.java
 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/http/QueryExceptionHTTP.java
index 0c14af343d..126857ddda 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/http/QueryExceptionHTTP.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/http/QueryExceptionHTTP.java
@@ -21,24 +21,25 @@
 
 package org.apache.jena.sparql.engine.http;
 
+import java.net.http.HttpHeaders;
+
 import org.apache.jena.atlas.web.HttpException;
 import org.apache.jena.query.QueryException;
 import org.apache.jena.web.HttpSC;
 
 /**
- * Exception class for all operations in the SPARQL client library. Error 
codes are
- * as HTTP status codes.
+ * Exception class for all HTTP operations in the SPARQL client library.
+ * Error codes are as HTTP status codes.
  */
 public class QueryExceptionHTTP extends QueryException
 {
     public static final int noStatusCode = -1234;
     private int statusCode = noStatusCode;
-    private final String responseMessage;
-    private String statusLine;
-    private String response;
+    private final String statusLine;
 
-    // Codes for extra errors.  We use HTTP error codes so
-    // these are negative to avoid clashes
+    private final String responseBody;
+    private final HttpHeaders responseHeaders;
+    // Codes for extra errors. We use HTTP error codes so these are negative 
to avoid clashes
     public static final int NoServer = -404;
 
     public static QueryExceptionHTTP rewrap(HttpException httpEx) {
@@ -46,8 +47,8 @@ public class QueryExceptionHTTP extends QueryException
         // ARQ machinery we use these days means the internal HTTP errors come 
back as HttpException
         // Therefore we need to wrap appropriately
         int responseCode = httpEx.getStatusCode();
-        if (responseCode != -1) {
-            // Was an actual HTTP error
+        if (responseCode > 0) {
+            // It was an actual HTTP error
             String responseLine = httpEx.getStatusLine() != null ? 
httpEx.getStatusLine() : HttpSC.getMessage(responseCode);
             return new QueryExceptionHTTP(responseCode, responseLine, httpEx);
         } else if (httpEx.getMessage() != null) {
@@ -62,25 +63,14 @@ public class QueryExceptionHTTP extends QueryException
         }
     }
 
-    /**
-     * Constructor for QueryExceptionHTTP.
-     * @param responseCode
-     * @param responseMessage
-     */
-    public QueryExceptionHTTP(int responseCode, String responseMessage) {
-        super(responseMessage);
-        this.statusCode = responseCode;
-        this.responseMessage = responseMessage;
-    }
-
-    /**
-     * Constructor for QueryExceptionHTTP.
-     * @param responseCode
-     */
-    public QueryExceptionHTTP(int responseCode) {
-        super();
+    /** @deprecated Use {@ink #wrap(HttpException)} */
+    @Deprecated
+    public QueryExceptionHTTP(int responseCode, String messageBody, final 
HttpException ex) {
+        super(ex.getMessage(), ex.getCause());
         this.statusCode = responseCode;
-        this.responseMessage = null;
+        this.statusLine = ex.getStatusLine();
+        this.responseBody = ex.getResponse();
+        this.responseHeaders = ex.getHttpResponseHeaders();
     }
 
     /** The code for the reason for this exception
@@ -90,52 +80,33 @@ public class QueryExceptionHTTP extends QueryException
 
     /** The message for the reason for this exception
      * @return message
+     * @deprecate Use {@link #getResponseBody}
      */
-    public String getResponseMessage() { return responseMessage; }
+    @Deprecated
+    public String getResponseMessage() { return responseBody; }
+
+    public HttpHeaders getResponseHeaders() { return responseHeaders; }
+
+    public String getResponseBody() { return responseBody; }
 
     /** The response for this exception if available from HTTP
      * @return response or {@code null} if no HTTP response was received
+     * @deprecate Use {@link #getResponseBody}
      */
-    public String getResponse() { return response; }
+    @Deprecated
+    public String getResponse() { return getResponseBody(); }
 
     /** The status line for the response for this exception if available from 
HTTP
      * @return status line or {@code null} if no HTTP response was received
      */
     public String getStatusLine() { return statusLine; }
 
-    /**
-     * Constructor for HttpException used for some unexpected execution error.
-     * @param cause
-     */
-    public QueryExceptionHTTP(Throwable cause) {
-        super(cause);
-        this.statusCode = noStatusCode;
-        this.responseMessage = null;
-    }
-
-    public QueryExceptionHTTP(String msg, Throwable cause) {
-        super(msg, cause);
-        this.statusCode = noStatusCode;
-        this.responseMessage = msg;
-    }
-
-    public QueryExceptionHTTP(int responseCode, String message, Throwable 
cause) {
-        this(message, cause);
-        this.statusCode = responseCode;
-    }
-
-    public QueryExceptionHTTP(int responseCode, String message, final 
HttpException ex) {
-        this(responseCode, message, ex.getCause());
-        this.statusLine = ex.getStatusLine();
-        this.response = ex.getResponse();
-    }
-
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append("HttpException: ");
         int code = getStatusCode();
-        if ( code != QueryExceptionHTTP.noStatusCode ) {
+        if ( code > 0 ) {
             sb.append(code);
             if ( getResponseMessage() != null ) {
                 sb.append(" ");
@@ -146,4 +117,51 @@ public class QueryExceptionHTTP extends QueryException
         }
         return sb.toString();
     }
+
+    // Older constructors, no longer used.
+
+    @Deprecated(forRemoval = true)
+    public QueryExceptionHTTP(int responseCode, String responseMessage) {
+        super(responseMessage);
+        this.statusCode = responseCode;
+        this.statusLine = responseMessage;
+        this.responseHeaders = null;
+        this.responseBody = null;
+    }
+
+    @Deprecated(forRemoval = true)
+    public QueryExceptionHTTP(int responseCode) {
+        super();
+        this.statusCode = responseCode;
+        this.statusLine = null;
+        this.responseHeaders = null;
+        this.responseBody = null;
+    }
+
+    @Deprecated(forRemoval = true)
+    public QueryExceptionHTTP(Throwable cause) {
+        super(cause);
+        this.statusCode = noStatusCode;
+        this.statusLine = null;
+        this.responseHeaders = null;
+        this.responseBody = null;
+    }
+
+    @Deprecated(forRemoval = true)
+    public QueryExceptionHTTP(String msg, Throwable cause) {
+        super(msg, cause);
+        this.statusCode = noStatusCode;
+        this.statusLine = null;
+        this.responseHeaders = null;
+        this.responseBody = null;
+    }
+
+    @Deprecated(forRemoval = true)
+    public QueryExceptionHTTP(int responseCode, String message, Throwable 
cause) {
+        super(message, cause);
+        this.statusCode = responseCode;
+        this.statusLine = null;
+        this.responseHeaders = null;
+        this.responseBody = null;
+    }
 }
diff --git 
a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecCleanServer.java
 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecCleanServer.java
index 29cd6a215f..8428080900 100644
--- 
a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecCleanServer.java
+++ 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecCleanServer.java
@@ -24,6 +24,7 @@ package org.apache.jena.sparql.exec.http;
 import static org.apache.jena.atlas.lib.StrUtils.strjoinNL;
 import static org.apache.jena.sparql.sse.SSE.parseQuad;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.concurrent.TimeUnit;
@@ -43,7 +44,7 @@ import org.apache.jena.sparql.engine.http.QueryExceptionHTTP;
 
 /**
  * Tests for {@link QueryExecHTTP} with no authentication.
- * See  {@link TestQueryExecHTTP} for most of the tests.
+ * See {@link TestQueryExecHTTP} for most of the tests.
  */
 public class TestQueryExecCleanServer {
     // Unlike TestQueryExecutionHTTP these tests run a clean server each time.
@@ -104,10 +105,12 @@ public class TestQueryExecCleanServer {
                                             // Short!
                                             .timeout(10, TimeUnit.MILLISECONDS)
                                             .build() ) {
-            assertThrows(QueryExceptionHTTP.class, ()->{
+            QueryExceptionHTTP ex = assertThrows(QueryExceptionHTTP.class, 
()->{
                 long x = Iter.count(qExec.select());
                 assertEquals(2, x);
             });
+            // Client side timeout.
+            assertNull(ex.getResponseHeaders());
         }
     }
 }
diff --git 
a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecHTTP.java
 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecHTTP.java
index ae30518ee9..79c29ac8ba 100644
--- 
a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecHTTP.java
+++ 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestQueryExecHTTP.java
@@ -24,11 +24,14 @@ package org.apache.jena.sparql.exec.http;
 import static org.apache.jena.sparql.sse.SSE.parseQuad;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.net.http.HttpHeaders;
 import java.util.Iterator;
 
 import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
@@ -42,10 +45,13 @@ import org.apache.jena.graph.Triple;
 import org.apache.jena.query.Query;
 import org.apache.jena.query.QueryFactory;
 import org.apache.jena.query.Syntax;
+import org.apache.jena.riot.web.HttpNames;
 import org.apache.jena.sparql.core.DatasetGraph;
 import org.apache.jena.sparql.core.DatasetGraphFactory;
 import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.engine.http.QueryExceptionHTTP;
 import org.apache.jena.sparql.exec.RowSet;
+import org.apache.jena.web.HttpSC;
 
 /**
  * Tests for {@link QueryExecHTTP} with no authentication.
@@ -99,6 +105,25 @@ public class TestQueryExecHTTP {
         }
     }
 
+    @Test
+    public void query_select_bad_01() {
+        LogCtl.withLevel(Fuseki.actionLog, "ERROR", ()->{
+
+            // Make a bad request
+            try ( QueryExecHTTP qExec = 
QueryExecHTTP.newBuilder().endpoint(dsURL).queryString("SELECT * { JUNK 
}").build() ) {
+                QueryExceptionHTTP ex = 
Assertions.assertThrows(QueryExceptionHTTP.class, ()->qExec.select());
+                assertEquals(HttpSC.BAD_REQUEST_400, ex.getStatusCode());
+                assertNotNull(ex.getResponseHeaders());
+                HttpHeaders headers = ex.getResponseHeaders();
+                // Fuseki includes the parse error.
+                assertNotNull(headers);
+                var x = headers.firstValue(HttpNames.hContentType);
+                assertFalse(x.isEmpty());
+                assertNotNull(x.get());
+            }
+        });
+    }
+
     @Test
     public void query_select_post_form_1() {
         try ( QueryExecHTTP qExec = 
QueryExecHTTP.newBuilder().sendMode(QuerySendMode.asPostForm)

Reply via email to