This is an automated email from the ASF dual-hosted git repository.
andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git
The following commit(s) were added to refs/heads/main by this push:
new 1136be12e6 Improved http content type issue exception message
new 54f2127dfe Merge pull request #1483 from Aklakan/gh-1482
1136be12e6 is described below
commit 1136be12e69f559e377568e0606760f20b2bd521
Author: Claus Stadler <[email protected]>
AuthorDate: Mon Aug 15 17:19:31 2022 +0200
Improved http content type issue exception message
---
.../jena/sparql/exec/http/QueryExecHTTP.java | 88 +++++++++++++++++-----
.../src/main/java/org/apache/jena/atlas/io/IO.java | 27 ++++++-
2 files changed, 92 insertions(+), 23 deletions(-)
diff --git
a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java
b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java
index 4ce48917d6..bfa08bf570 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecHTTP.java
@@ -26,6 +26,7 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
@@ -40,6 +41,7 @@ import org.apache.jena.atlas.lib.InternalErrorException;
import org.apache.jena.atlas.lib.Pair;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.MediaType;
import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Triple;
import org.apache.jena.http.HttpEnv;
@@ -58,6 +60,7 @@ import org.apache.jena.sparql.engine.http.QueryExceptionHTTP;
import org.apache.jena.sparql.exec.QueryExec;
import org.apache.jena.sparql.exec.RowSet;
import org.apache.jena.sparql.util.Context;
+import org.apache.jena.web.HttpSC;
/**
* A {@link QueryExec} implementation where queries are executed against a
remote
@@ -165,7 +168,8 @@ public class QueryExecHTTP implements QueryExec {
// Use the explicitly given header or the default selectAcceptheader
String thisAcceptHeader = dft(appProvidedAcceptHeader,
selectAcceptheader);
- HttpResponse<InputStream> response = performQuery(thisAcceptHeader);
+ HttpRequest request = effectiveHttpRequest(thisAcceptHeader);
+ HttpResponse<InputStream> response = executeQuery(request);
InputStream in = HttpLib.getInputStream(response);
// Don't assume the endpoint actually gives back the content type we
asked for
String actualContentType = responseHeader(response,
HttpNames.hContentType);
@@ -190,10 +194,15 @@ public class QueryExecHTTP implements QueryExec {
// Map to lang, with pragmatic alternatives.
Lang lang = WebContent.contentTypeToLangResultSet(actualContentType);
- if ( lang == null )
- throw new QueryException("Endpoint returned Content-Type: " +
actualContentType + " which is not recognized for SELECT queries");
- if ( !ResultSetReaderRegistry.isRegistered(lang) )
- throw new QueryException("Endpoint returned Content-Type: " +
actualContentType + " which is not supported for SELECT queries");
+ boolean unknownLang = lang == null;
+ boolean unsupportedFormat = !unknownLang &&
!ResultSetReaderRegistry.isRegistered(lang);
+ if ( unknownLang || unsupportedFormat ) {
+ String errorTerm = unknownLang ? "recognized" : "supported";
+ String errorMsg = String.format("Endpoint returned Content-Type:
%s which is not %s for SELECT queries",
+ actualContentType, errorTerm);
+ raiseException(errorMsg, request, response, in);
+ }
+
// This returns a streaming result set for some formats.
// Do not close the InputStream at this point.
ResultSet result = ResultSetMgr.read(in, lang);
@@ -205,7 +214,8 @@ public class QueryExecHTTP implements QueryExec {
checkNotClosed();
check(QueryType.ASK);
String thisAcceptHeader = dft(appProvidedAcceptHeader,
askAcceptHeader);
- HttpResponse<InputStream> response = performQuery(thisAcceptHeader);
+ HttpRequest request = effectiveHttpRequest(thisAcceptHeader);
+ HttpResponse<InputStream> response = executeQuery(request);
InputStream in = HttpLib.getInputStream(response);
String actualContentType = responseHeader(response,
HttpNames.hContentType);
@@ -227,8 +237,9 @@ public class QueryExecHTTP implements QueryExec {
else if ( actualContentType.equals(WebContent.contentTypeJSON))
lang = ResultSetLang.RS_JSON;
}
- if ( lang == null )
- throw new QueryException("Endpoint returned Content-Type: " +
actualContentType + " which is not supported for ASK queries");
+ if (lang == null) {
+ raiseException("Endpoint returned Content-Type: " +
actualContentType + " which is not supported for ASK queries", request,
response, in);
+ }
boolean result = ResultSetMgr.readBoolean(in, lang);
finish(in);
return result;
@@ -340,7 +351,8 @@ public class QueryExecHTTP implements QueryExec {
private Pair<InputStream, Lang> execRdfWorker(String contentType, String
ifNoContentType) {
checkNotClosed();
String thisAcceptHeader = dft(appProvidedAcceptHeader, contentType);
- HttpResponse<InputStream> response = performQuery(thisAcceptHeader);
+ HttpRequest request = effectiveHttpRequest(thisAcceptHeader);
+ HttpResponse<InputStream> response = executeQuery(request);
InputStream in = HttpLib.getInputStream(response);
// Don't assume the endpoint actually gives back the content type we
asked for
@@ -354,10 +366,11 @@ public class QueryExecHTTP implements QueryExec {
actualContentType = ifNoContentType;
Lang lang = RDFLanguages.contentTypeToLang(actualContentType);
- if ( ! RDFLanguages.isQuads(lang) && ! RDFLanguages.isTriples(lang) )
- throw new QueryException("Endpoint returned Content Type: "
+ if ( ! RDFLanguages.isQuads(lang) && ! RDFLanguages.isTriples(lang) ) {
+ raiseException("Endpoint returned Content Type: "
+ actualContentType
- + " which is not a valid RDF syntax");
+ + " which is not a valid RDF syntax", request, response,
in);
+ }
return Pair.create(in, lang);
}
@@ -366,7 +379,8 @@ public class QueryExecHTTP implements QueryExec {
checkNotClosed();
check(QueryType.CONSTRUCT_JSON);
String thisAcceptHeader = dft(appProvidedAcceptHeader,
WebContent.contentTypeJSON);
- HttpResponse<InputStream> response = performQuery(thisAcceptHeader);
+ HttpRequest request = effectiveHttpRequest(thisAcceptHeader);
+ HttpResponse<InputStream> response = executeQuery(request);
InputStream in = HttpLib.getInputStream(response);
try {
return JSON.parseAny(in).getAsArray();
@@ -441,13 +455,42 @@ public class QueryExecHTTP implements QueryExec {
return (duration < 0) ? duration : timeUnit.toMillis(duration);
}
+ private void raiseException(String errorMsg, HttpRequest request,
HttpResponse<?> response, InputStream in) {
+ int bodySummaryLength = 1024;
+ int statusCode = response.statusCode();
+ String statusCodeMsg = HttpSC.getMessage(statusCode);
+
+ // Determine the charset for extracting an excerpt of the body
+ String actualContentType = responseHeader(response,
HttpNames.hContentType);
+ MediaType ct = MediaType.create(actualContentType);
+ String charsetName = ct == null ? null : ct.getCharset();
+ Charset charset = null;
+ try {
+ charset = charsetName == null ? null :
Charset.forName(charsetName);
+ } catch (Throwable e) {
+ // Silently ignore
+ }
+ if (charset == null) {
+ charset = StandardCharsets.UTF_8;
+ }
+
+ String bodyStr;
+ try {
+ bodyStr = in == null ? "(no data supplied)" : IO.abbreviate(in,
charset, bodySummaryLength, "...");
+ } catch (Throwable e) {
+ // No need to rethrow because we are already about to throw
+ bodyStr = "(failed to retrieve HTTP body due to: " +
e.getMessage() + ")";
+ }
+
+ throw new QueryException(String.format(
+ "%s.\nStatus code %d %s, Method %s, Request Headers: %s\nBody
(extracted with charset %s): %s",
+ errorMsg, statusCode, statusCodeMsg, request.method(),
request.headers().map(), charset.name(), bodyStr));
+ }
+
/**
- * Make a query over HTTP.
- * The response is returned after status code processing so the caller can
assume the
- * query execution was successful and return 200.
- * Use {@link HttpLib#getInputStream} to access the body.
+ * Build the effective HTTP request ready for use with {@link
#executeQuery(HttpRequest)}.
*/
- private HttpResponse<InputStream> performQuery(String reqAcceptHeader) {
+ private HttpRequest effectiveHttpRequest(String reqAcceptHeader) {
if (closed)
throw new ARQException("HTTP execution already closed");
@@ -467,8 +510,7 @@ public class QueryExecHTTP implements QueryExec {
HttpLib.modifyByService(service, context, thisParams, httpHeaders);
HttpRequest request = makeRequest(thisParams, reqAcceptHeader);
-
- return executeQuery(request);
+ return request;
}
private HttpRequest makeRequest(Params thisParams, String reqAcceptHeader)
{
@@ -491,6 +533,12 @@ public class QueryExecHTTP implements QueryExec {
return requestBuilder.build();
}
+ /**
+ * Execute an HttpRequest.
+ * The response is returned after status code processing so the caller can
assume the
+ * query execution was successful and return 200.
+ * Use {@link HttpLib#getInputStream} to access the body.
+ */
private HttpResponse<InputStream> executeQuery(HttpRequest request) {
logQuery(queryString, request);
try {
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/io/IO.java
b/jena-base/src/main/java/org/apache/jena/atlas/io/IO.java
index b0c97a512d..8cce6f9e95 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/io/IO.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/io/IO.java
@@ -30,6 +30,7 @@ import java.util.zip.GZIPOutputStream;
import
org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import
org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import
org.apache.commons.compress.compressors.snappy.SnappyCompressorInputStream;
+import org.apache.commons.io.IOUtils;
import org.apache.jena.atlas.RuntimeIOException;
import org.apache.jena.atlas.lib.IRILib;
import org.apache.jena.atlas.lib.StrUtils;
@@ -416,7 +417,6 @@ public class IO
* @param filename
* @return String
*/
-
public static String readWholeFileAsUTF8(String filename) {
try ( InputStream in = new FileInputStream(filename) ) {
return readWholeFileAsUTF8(in);
@@ -441,13 +441,35 @@ public class IO
}
}
+ /** Fully reads the next up to maxWidth + 1 characters from the stream and
returns them as a string.
+ * If the extra character is read then the apprevMarker in appended to the
result in its place.
+ * Closing the stream is the caller's responsibility.
+ */
+ public static String abbreviate(InputStream in, Charset charset, int
maxWidth, String abbrevMarker) throws IOException {
+ return abbreviate(new InputStreamReader(in, charset), maxWidth,
abbrevMarker);
+ }
+
+ /** Fully reads the next up to maxWidth + 1 characters from the reader and
returns them as a string.
+ * If the extra character is read then the apprevMarker in appended to the
result in its place.
+ * Closing the stream is the caller's responsibility.
+ */
+ public static String abbreviate(Reader reader, int maxWidth, String
abbrevMarker) throws IOException {
+ char[] buffer = new char[maxWidth + 1];
+ int n = IOUtils.read(reader, buffer);
+ StringBuilder sb = new StringBuilder();
+ sb.append(buffer, 0, Math.min(n, maxWidth));
+ if (n > maxWidth) {
+ sb.append(abbrevMarker);
+ }
+ return sb.toString();
+ }
+
/** Read a whole file as UTF-8
*
* @param r
* @return String The whole file
* @throws IOException
*/
-
// Private worker as we are trying to force UTF-8.
private static String readWholeFileAsUTF8(Reader r) throws IOException {
final int WHOLE_FILE_BUFFER_SIZE = 32*1024;
@@ -469,7 +491,6 @@ public class IO
* @param content String to be written
* @throws IOException
*/
-
public static void writeStringAsUTF8(String filename, String content)
throws IOException {
try ( OutputStream out = IO.openOutputFileEx(filename) ) {
writeStringAsUTF8(out, content);