From 45b814ac05543f62df1cd23e44243b57cfb1e6b8 Mon Sep 17 00:00:00 2001 From: Jeff Thomas Date: Tue, 8 Apr 2025 21:16:32 +0200 Subject: [PATCH 1/2] AXIS2-6091 - Handle 400-500 errors with content-type "text/html" Addresses an issue where non-SOAP HTTP error responses (4xx-5xx) with a "text/html" content type were not being properly handled, resulting in uninformative error messages. Now, the response body from these errors is extracted and included in the AxisFault detail, providing more context for debugging. This ensures that users receive more meaningful error information when such errors occur. --- .../axis2/kernel/http/HTTPConstants.java | 50 +++++ .../axis2/transport/http/HTTPSender.java | 179 +++++++++++++++++- 2 files changed, 223 insertions(+), 6 deletions(-) diff --git a/modules/kernel/src/org/apache/axis2/kernel/http/HTTPConstants.java b/modules/kernel/src/org/apache/axis2/kernel/http/HTTPConstants.java index 9a11dc4677..4cef4ec826 100644 --- a/modules/kernel/src/org/apache/axis2/kernel/http/HTTPConstants.java +++ b/modules/kernel/src/org/apache/axis2/kernel/http/HTTPConstants.java @@ -21,6 +21,7 @@ package org.apache.axis2.kernel.http; import java.io.UnsupportedEncodingException; +import javax.xml.namespace.QName; /** * HTTP protocol and message context constants. @@ -533,4 +534,53 @@ public static byte[] getBytes(final String data) { public static final String USER_AGENT = "userAgent"; public static final String SERVER = "server"; + + /** Base QName namespace for HTTP errors. */ + public static final String QNAME_HTTP_NS = + "http://ws.apache.org/axis2/http"; + + /** QName for faults caused by a 400 Bad Request HTTP response. */ + public static final QName QNAME_HTTP_BAD_REQUEST = + new QName(QNAME_HTTP_NS, "BAD_REQUEST"); + + /** QName for faults caused by a 401 Unauthorized HTTP response. */ + public static final QName QNAME_HTTP_UNAUTHORIZED = + new QName(QNAME_HTTP_NS, "UNAUTHORIZED"); + + /** QName for faults caused by a 403 Forbidden HTTP response. */ + public static final QName QNAME_HTTP_FORBIDDEN = + new QName(QNAME_HTTP_NS, "FORBIDDEN"); + + /** QName for faults caused by a 404 Not Found HTTP response. */ + public static final QName QNAME_HTTP_NOT_FOUND = + new QName(QNAME_HTTP_NS, "NOT_FOUND"); + + /** QName for faults caused by a 405 Method Not Allowed HTTP response. */ + public static final QName QNAME_HTTP_METHOD_NOT_ALLOWED = + new QName(QNAME_HTTP_NS, "METHOD_NOT_ALLOWED"); + + /** QName for faults caused by a 406 Not Acceptable HTTP response. */ + public static final QName QNAME_HTTP_NOT_ACCEPTABLE = + new QName(QNAME_HTTP_NS, "NOT_ACCEPTABLE"); + + /** QName for faults caused by a 407 Proxy Authentication Required HTTP response. */ + public static final QName QNAME_HTTP_PROXY_AUTH_REQUIRED = + new QName(QNAME_HTTP_NS, "PROXY_AUTHENTICATION_REQUIRED"); + + /** QName for faults caused by a 408 Request Timeout HTTP response. */ + public static final QName QNAME_HTTP_REQUEST_TIMEOUT = + new QName(QNAME_HTTP_NS, "REQUEST_TIMEOUT"); + + /** QName for faults caused by a 409 Conflict HTTP response. */ + public static final QName QNAME_HTTP_CONFLICT = + new QName(QNAME_HTTP_NS, "CONFLICT"); + + /** QName for faults caused by a 410 Gone HTTP response. */ + public static final QName QNAME_HTTP_GONE = + new QName(QNAME_HTTP_NS, "GONE"); + + /** QName for faults caused by a 500 Internal Server Error HTTP response. */ + public static final QName QNAME_HTTP_INTERNAL_SERVER_ERROR = + new QName(QNAME_HTTP_NS, "INTERNAL_SERVER_ERROR"); + } diff --git a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java index b1e9c28a76..2c56cd8cb2 100644 --- a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java +++ b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java @@ -22,6 +22,7 @@ import org.apache.axiom.mime.ContentType; import org.apache.axiom.mime.Header; +import org.apache.axiom.om.OMAbstractFactory; import org.apache.axiom.om.OMAttribute; import org.apache.axiom.om.OMElement; import org.apache.axiom.om.OMOutputFormat; @@ -43,19 +44,36 @@ import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpHeaders; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; import java.text.ParseException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import javax.xml.namespace.QName; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_BAD_REQUEST; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_CONFLICT; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_FORBIDDEN; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_GONE; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_INTERNAL_SERVER_ERROR; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_METHOD_NOT_ALLOWED; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_NOT_ACCEPTABLE; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_NOT_FOUND; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_PROXY_AUTH_REQUIRED; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_REQUEST_TIMEOUT; +import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_UNAUTHORIZED; + //TODO - It better if we can define these method in a interface move these into AbstractHTTPSender and get rid of this class. public abstract class HTTPSender { @@ -196,7 +214,9 @@ public void send(MessageContext msgContext, URL url, String soapActionString) boolean cleanup = true; try { int statusCode = request.getStatusCode(); - log.trace("Handling response - " + statusCode); + + log.trace("Handling response - [content-type='" + contentType + "', statusCode=" + statusCode + "]"); + boolean processResponse; boolean fault; if (statusCode == HttpStatus.SC_ACCEPTED) { @@ -205,14 +225,22 @@ public void send(MessageContext msgContext, URL url, String soapActionString) } else if (statusCode >= 200 && statusCode < 300) { processResponse = true; fault = false; - } else if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR - || statusCode == HttpStatus.SC_BAD_REQUEST || statusCode == HttpStatus.SC_NOT_FOUND) { - processResponse = true; - fault = true; + } else if (statusCode >= 400 && statusCode <= 500) { + + // if the response has a HTTP error code (401/404/500) but is *not* a SOAP response, handle it here + if (contentType != null && contentType.startsWith("text/html")) { + throw handleNonSoapError(request, statusCode); + } else { + processResponse = true; + fault = true; + } + } else { - throw new AxisFault(Messages.getMessage("transportError", String.valueOf(statusCode), + throw new AxisFault(Messages.getMessage("transportError", + String.valueOf(statusCode), request.getStatusText())); } + obtainHTTPHeaderInformation(request, msgContext); if (processResponse) { OperationContext opContext = msgContext.getOperationContext(); @@ -498,4 +526,143 @@ private String buildCookieString(Map cookies, String name) { String value = cookies.get(name); return value == null ? null : name + "=" + value; } + + /** + * Handles non-SOAP HTTP error responses (e.g., 404, 500) by creating an AxisFault. + *

+ * If the response is `text/html`, it extracts the response body and includes it + * as fault details, wrapped within a CDATA block. + *

+ * + * @param request the HTTP request instance + * @param statusCode the HTTP status code + * @return AxisFault containing the error details + */ + private AxisFault handleNonSoapError(final Request request, final int statusCode) { + + String responseContent = null; + + InputStream responseContentInputStream = null; + try { + responseContentInputStream = request.getResponseContent(); + } catch (final IOException ex) { + // NO-OP + } + + if (responseContentInputStream != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseContentInputStream))) { + responseContent = reader.lines().collect(Collectors.joining("\n")).trim(); + } catch (IOException e) { + log.warn("Failed to read response content from HTTP error response", e); + } + } + + // Build and throw an AxisFault with the response content + final String faultMessage = + Messages.getMessage("transportError", String.valueOf(statusCode), responseContent); + + final QName faultQName = getFaultQNameForStatusCode(statusCode).orElse(null); + + final AxisFault fault = new AxisFault(faultMessage, faultQName); + final OMElement faultDetail = createFaultDetailForNonSoapError(responseContent); + fault.setDetail(faultDetail); + + return fault; + + } + + /** + * Returns an appropriate QName for the given HTTP status code. + * + * @param statusCode the HTTP status code (e.g., 404, 500) + * @return an Optional containing the QName if available, or an empty Optional if the status code is unsupported + */ + private Optional getFaultQNameForStatusCode(int statusCode) { + + final QName faultQName; + + switch (statusCode) { + case HttpStatus.SC_BAD_REQUEST: + faultQName = QNAME_HTTP_BAD_REQUEST; + break; + case HttpStatus.SC_UNAUTHORIZED: + faultQName = QNAME_HTTP_UNAUTHORIZED; + break; + case HttpStatus.SC_FORBIDDEN: + faultQName = QNAME_HTTP_FORBIDDEN; + break; + case HttpStatus.SC_NOT_FOUND: + faultQName = QNAME_HTTP_NOT_FOUND; + break; + case HttpStatus.SC_METHOD_NOT_ALLOWED: + faultQName = QNAME_HTTP_METHOD_NOT_ALLOWED; + break; + case HttpStatus.SC_NOT_ACCEPTABLE: + faultQName = QNAME_HTTP_NOT_ACCEPTABLE; + break; + case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: + faultQName = QNAME_HTTP_PROXY_AUTH_REQUIRED; + break; + case HttpStatus.SC_REQUEST_TIMEOUT: + faultQName = QNAME_HTTP_REQUEST_TIMEOUT; + break; + case HttpStatus.SC_CONFLICT: + faultQName = QNAME_HTTP_CONFLICT; + break; + case HttpStatus.SC_GONE: + faultQName = QNAME_HTTP_GONE; + break; + case HttpStatus.SC_INTERNAL_SERVER_ERROR: + faultQName = QNAME_HTTP_INTERNAL_SERVER_ERROR; + break; + default: + faultQName = null; + break; + } + + return Optional.ofNullable(faultQName); + + } + + /** + * Creates a fault detail element containing the response content. + */ + private OMElement createFaultDetailForNonSoapError(String responseContent) { + + final OMElement faultDetail = + OMAbstractFactory.getOMFactory().createOMElement(new QName("http://ws.apache.org/axis2", "Details")); + + final OMElement textNode = + OMAbstractFactory.getOMFactory().createOMElement(new QName("http://ws.apache.org/axis2", "Text")); + + if (responseContent != null && !responseContent.isEmpty()) { + textNode.setText(wrapResponseWithCDATA(responseContent)); + } else { + textNode.setText(wrapResponseWithCDATA("The endpoint returned no response content.")); + } + + faultDetail.addChild(textNode); + + return faultDetail; + + } + + /** + * Wraps the given HTML response content in a CDATA block to allow it to be added as Text in a fault-detail. + * + * @param responseContent the response content + * @return the CDATA-wrapped response + */ + private String wrapResponseWithCDATA(final String responseContent) { + + if (responseContent == null || responseContent.isEmpty()) { + return ""; + } + + // Replace closing CDATA sequences properly + String safeContent = responseContent.replace("]]>", "]]]]>").replace("\n", " "); + return ""; + + } + } From 629a86fe06ac312ee7671ca43501d783ffd3ecc5 Mon Sep 17 00:00:00 2001 From: Jeff Thomas Date: Mon, 14 Apr 2025 12:21:30 +0200 Subject: [PATCH 2/2] AXIS2-6091 - Minor correcitons --- .../main/java/org/apache/axis2/transport/http/HTTPSender.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java index 2c56cd8cb2..34ec148cb9 100644 --- a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java +++ b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java @@ -41,6 +41,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpHeaders; @@ -54,7 +55,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -294,7 +294,7 @@ public void send(MessageContext msgContext, URL url, String soapActionString) log.info("Unable to send to url[" + url + "]", e); throw AxisFault.makeFault(e); } - } + } private void addCustomHeaders(MessageContext msgContext, Request request) {