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)
