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);

Reply via email to