Skip to content

Commit 45b814a

Browse files
committed
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.
1 parent d2a8df7 commit 45b814a

2 files changed

Lines changed: 223 additions & 6 deletions

File tree

modules/kernel/src/org/apache/axis2/kernel/http/HTTPConstants.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package org.apache.axis2.kernel.http;
2222

2323
import java.io.UnsupportedEncodingException;
24+
import javax.xml.namespace.QName;
2425

2526
/**
2627
* HTTP protocol and message context constants.
@@ -533,4 +534,53 @@ public static byte[] getBytes(final String data) {
533534

534535
public static final String USER_AGENT = "userAgent";
535536
public static final String SERVER = "server";
537+
538+
/** Base QName namespace for HTTP errors. */
539+
public static final String QNAME_HTTP_NS =
540+
"http://ws.apache.org/axis2/http";
541+
542+
/** QName for faults caused by a 400 Bad Request HTTP response. */
543+
public static final QName QNAME_HTTP_BAD_REQUEST =
544+
new QName(QNAME_HTTP_NS, "BAD_REQUEST");
545+
546+
/** QName for faults caused by a 401 Unauthorized HTTP response. */
547+
public static final QName QNAME_HTTP_UNAUTHORIZED =
548+
new QName(QNAME_HTTP_NS, "UNAUTHORIZED");
549+
550+
/** QName for faults caused by a 403 Forbidden HTTP response. */
551+
public static final QName QNAME_HTTP_FORBIDDEN =
552+
new QName(QNAME_HTTP_NS, "FORBIDDEN");
553+
554+
/** QName for faults caused by a 404 Not Found HTTP response. */
555+
public static final QName QNAME_HTTP_NOT_FOUND =
556+
new QName(QNAME_HTTP_NS, "NOT_FOUND");
557+
558+
/** QName for faults caused by a 405 Method Not Allowed HTTP response. */
559+
public static final QName QNAME_HTTP_METHOD_NOT_ALLOWED =
560+
new QName(QNAME_HTTP_NS, "METHOD_NOT_ALLOWED");
561+
562+
/** QName for faults caused by a 406 Not Acceptable HTTP response. */
563+
public static final QName QNAME_HTTP_NOT_ACCEPTABLE =
564+
new QName(QNAME_HTTP_NS, "NOT_ACCEPTABLE");
565+
566+
/** QName for faults caused by a 407 Proxy Authentication Required HTTP response. */
567+
public static final QName QNAME_HTTP_PROXY_AUTH_REQUIRED =
568+
new QName(QNAME_HTTP_NS, "PROXY_AUTHENTICATION_REQUIRED");
569+
570+
/** QName for faults caused by a 408 Request Timeout HTTP response. */
571+
public static final QName QNAME_HTTP_REQUEST_TIMEOUT =
572+
new QName(QNAME_HTTP_NS, "REQUEST_TIMEOUT");
573+
574+
/** QName for faults caused by a 409 Conflict HTTP response. */
575+
public static final QName QNAME_HTTP_CONFLICT =
576+
new QName(QNAME_HTTP_NS, "CONFLICT");
577+
578+
/** QName for faults caused by a 410 Gone HTTP response. */
579+
public static final QName QNAME_HTTP_GONE =
580+
new QName(QNAME_HTTP_NS, "GONE");
581+
582+
/** QName for faults caused by a 500 Internal Server Error HTTP response. */
583+
public static final QName QNAME_HTTP_INTERNAL_SERVER_ERROR =
584+
new QName(QNAME_HTTP_NS, "INTERNAL_SERVER_ERROR");
585+
536586
}

modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPSender.java

Lines changed: 173 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import org.apache.axiom.mime.ContentType;
2424
import org.apache.axiom.mime.Header;
25+
import org.apache.axiom.om.OMAbstractFactory;
2526
import org.apache.axiom.om.OMAttribute;
2627
import org.apache.axiom.om.OMElement;
2728
import org.apache.axiom.om.OMOutputFormat;
@@ -43,19 +44,36 @@
4344
import org.apache.hc.core5.http.HttpStatus;
4445
import org.apache.hc.core5.http.HttpHeaders;
4546

47+
import java.io.BufferedReader;
4648
import java.io.IOException;
4749
import java.io.InputStream;
50+
import java.io.InputStreamReader;
4851
import java.net.URL;
4952
import java.text.ParseException;
5053
import java.util.HashMap;
5154
import java.util.Iterator;
5255
import java.util.List;
5356
import java.util.Map;
57+
import java.util.Objects;
58+
import java.util.Optional;
5459
import java.util.Set;
60+
import java.util.stream.Collectors;
5561
import java.util.zip.GZIPInputStream;
5662

5763
import javax.xml.namespace.QName;
5864

65+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_BAD_REQUEST;
66+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_CONFLICT;
67+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_FORBIDDEN;
68+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_GONE;
69+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_INTERNAL_SERVER_ERROR;
70+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_METHOD_NOT_ALLOWED;
71+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_NOT_ACCEPTABLE;
72+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_NOT_FOUND;
73+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_PROXY_AUTH_REQUIRED;
74+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_REQUEST_TIMEOUT;
75+
import static org.apache.axis2.kernel.http.HTTPConstants.QNAME_HTTP_UNAUTHORIZED;
76+
5977
//TODO - It better if we can define these method in a interface move these into AbstractHTTPSender and get rid of this class.
6078
public abstract class HTTPSender {
6179

@@ -196,7 +214,9 @@ public void send(MessageContext msgContext, URL url, String soapActionString)
196214
boolean cleanup = true;
197215
try {
198216
int statusCode = request.getStatusCode();
199-
log.trace("Handling response - " + statusCode);
217+
218+
log.trace("Handling response - [content-type='" + contentType + "', statusCode=" + statusCode + "]");
219+
200220
boolean processResponse;
201221
boolean fault;
202222
if (statusCode == HttpStatus.SC_ACCEPTED) {
@@ -205,14 +225,22 @@ public void send(MessageContext msgContext, URL url, String soapActionString)
205225
} else if (statusCode >= 200 && statusCode < 300) {
206226
processResponse = true;
207227
fault = false;
208-
} else if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
209-
|| statusCode == HttpStatus.SC_BAD_REQUEST || statusCode == HttpStatus.SC_NOT_FOUND) {
210-
processResponse = true;
211-
fault = true;
228+
} else if (statusCode >= 400 && statusCode <= 500) {
229+
230+
// if the response has a HTTP error code (401/404/500) but is *not* a SOAP response, handle it here
231+
if (contentType != null && contentType.startsWith("text/html")) {
232+
throw handleNonSoapError(request, statusCode);
233+
} else {
234+
processResponse = true;
235+
fault = true;
236+
}
237+
212238
} else {
213-
throw new AxisFault(Messages.getMessage("transportError", String.valueOf(statusCode),
239+
throw new AxisFault(Messages.getMessage("transportError",
240+
String.valueOf(statusCode),
214241
request.getStatusText()));
215242
}
243+
216244
obtainHTTPHeaderInformation(request, msgContext);
217245
if (processResponse) {
218246
OperationContext opContext = msgContext.getOperationContext();
@@ -498,4 +526,143 @@ private String buildCookieString(Map<String,String> cookies, String name) {
498526
String value = cookies.get(name);
499527
return value == null ? null : name + "=" + value;
500528
}
529+
530+
/**
531+
* Handles non-SOAP HTTP error responses (e.g., 404, 500) by creating an AxisFault.
532+
* <p>
533+
* If the response is `text/html`, it extracts the response body and includes it
534+
* as fault details, wrapped within a CDATA block.
535+
* </p>
536+
*
537+
* @param request the HTTP request instance
538+
* @param statusCode the HTTP status code
539+
* @return AxisFault containing the error details
540+
*/
541+
private AxisFault handleNonSoapError(final Request request, final int statusCode) {
542+
543+
String responseContent = null;
544+
545+
InputStream responseContentInputStream = null;
546+
try {
547+
responseContentInputStream = request.getResponseContent();
548+
} catch (final IOException ex) {
549+
// NO-OP
550+
}
551+
552+
if (responseContentInputStream != null) {
553+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseContentInputStream))) {
554+
responseContent = reader.lines().collect(Collectors.joining("\n")).trim();
555+
} catch (IOException e) {
556+
log.warn("Failed to read response content from HTTP error response", e);
557+
}
558+
}
559+
560+
// Build and throw an AxisFault with the response content
561+
final String faultMessage =
562+
Messages.getMessage("transportError", String.valueOf(statusCode), responseContent);
563+
564+
final QName faultQName = getFaultQNameForStatusCode(statusCode).orElse(null);
565+
566+
final AxisFault fault = new AxisFault(faultMessage, faultQName);
567+
final OMElement faultDetail = createFaultDetailForNonSoapError(responseContent);
568+
fault.setDetail(faultDetail);
569+
570+
return fault;
571+
572+
}
573+
574+
/**
575+
* Returns an appropriate QName for the given HTTP status code.
576+
*
577+
* @param statusCode the HTTP status code (e.g., 404, 500)
578+
* @return an Optional containing the QName if available, or an empty Optional if the status code is unsupported
579+
*/
580+
private Optional<QName> getFaultQNameForStatusCode(int statusCode) {
581+
582+
final QName faultQName;
583+
584+
switch (statusCode) {
585+
case HttpStatus.SC_BAD_REQUEST:
586+
faultQName = QNAME_HTTP_BAD_REQUEST;
587+
break;
588+
case HttpStatus.SC_UNAUTHORIZED:
589+
faultQName = QNAME_HTTP_UNAUTHORIZED;
590+
break;
591+
case HttpStatus.SC_FORBIDDEN:
592+
faultQName = QNAME_HTTP_FORBIDDEN;
593+
break;
594+
case HttpStatus.SC_NOT_FOUND:
595+
faultQName = QNAME_HTTP_NOT_FOUND;
596+
break;
597+
case HttpStatus.SC_METHOD_NOT_ALLOWED:
598+
faultQName = QNAME_HTTP_METHOD_NOT_ALLOWED;
599+
break;
600+
case HttpStatus.SC_NOT_ACCEPTABLE:
601+
faultQName = QNAME_HTTP_NOT_ACCEPTABLE;
602+
break;
603+
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
604+
faultQName = QNAME_HTTP_PROXY_AUTH_REQUIRED;
605+
break;
606+
case HttpStatus.SC_REQUEST_TIMEOUT:
607+
faultQName = QNAME_HTTP_REQUEST_TIMEOUT;
608+
break;
609+
case HttpStatus.SC_CONFLICT:
610+
faultQName = QNAME_HTTP_CONFLICT;
611+
break;
612+
case HttpStatus.SC_GONE:
613+
faultQName = QNAME_HTTP_GONE;
614+
break;
615+
case HttpStatus.SC_INTERNAL_SERVER_ERROR:
616+
faultQName = QNAME_HTTP_INTERNAL_SERVER_ERROR;
617+
break;
618+
default:
619+
faultQName = null;
620+
break;
621+
}
622+
623+
return Optional.ofNullable(faultQName);
624+
625+
}
626+
627+
/**
628+
* Creates a fault detail element containing the response content.
629+
*/
630+
private OMElement createFaultDetailForNonSoapError(String responseContent) {
631+
632+
final OMElement faultDetail =
633+
OMAbstractFactory.getOMFactory().createOMElement(new QName("http://ws.apache.org/axis2", "Details"));
634+
635+
final OMElement textNode =
636+
OMAbstractFactory.getOMFactory().createOMElement(new QName("http://ws.apache.org/axis2", "Text"));
637+
638+
if (responseContent != null && !responseContent.isEmpty()) {
639+
textNode.setText(wrapResponseWithCDATA(responseContent));
640+
} else {
641+
textNode.setText(wrapResponseWithCDATA("The endpoint returned no response content."));
642+
}
643+
644+
faultDetail.addChild(textNode);
645+
646+
return faultDetail;
647+
648+
}
649+
650+
/**
651+
* Wraps the given HTML response content in a CDATA block to allow it to be added as Text in a fault-detail.
652+
*
653+
* @param responseContent the response content
654+
* @return the CDATA-wrapped response
655+
*/
656+
private String wrapResponseWithCDATA(final String responseContent) {
657+
658+
if (responseContent == null || responseContent.isEmpty()) {
659+
return "<![CDATA[The endpoint returned no response content.]]>";
660+
}
661+
662+
// Replace closing CDATA sequences properly
663+
String safeContent = responseContent.replace("]]>", "]]]]><![CDATA[>").replace("\n", "&#10;");
664+
return "<![CDATA[" + safeContent + "]]>";
665+
666+
}
667+
501668
}

0 commit comments

Comments
 (0)