001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client;
018
019import java.io.*;
020import java.net.*;
021import java.nio.channels.*;
022import java.util.*;
023
024/**
025 * Thrown by {@link HttpTransport#execute(TransportRequest)} when a network-level error occurs (e.g. connection
026 * refused, timeout, SSL handshake failure).
027 *
028 * <p>
029 * This exception wraps the underlying transport library's exception so that callers do not need to handle
030 * Apache HttpClient, OkHttp, or JDK {@code HttpClient} exceptions directly.
031 *
032 * @since 9.2.1
033 */
034public class TransportException extends IOException {
035
036   private static final long serialVersionUID = 1L;
037
038   /**
039    * Constructor.
040    *
041    * @param message The error message.
042    */
043   public TransportException(String message) {
044      super(message);
045   }
046
047   /**
048    * Constructor.
049    *
050    * @param message The error message.
051    * @param cause The underlying cause. Can be <jk>null</jk> (no cause is chained).
052    */
053   public TransportException(String message, Throwable cause) {
054      super(message, cause);
055   }
056
057   /**
058    * Constructor.
059    *
060    * @param cause The underlying cause. Can be <jk>null</jk> (no cause is chained).
061    */
062   public TransportException(Throwable cause) {
063      super(cause);
064   }
065
066   /**
067    * Returns <jk>true</jk> if the given throwable (or any cause in its chain) is the signature of a pre-response
068    * stale pooled-connection failure — the server tore down a kept-alive connection before sending any response
069    * bytes.
070    *
071    * <p>
072    * This is recognized across transports as an {@link EOFException} or {@link ClosedChannelException}, a
073    * connection-reset {@link SocketException}, or the JDK {@link java.net.http.HttpClient}'s
074    * {@code "HTTP/1.1 header parser received no bytes"} message.  Such a failure is safe to replay exactly once on
075    * a fresh connection provided the request cannot cause a duplicate side effect (an idempotent HTTP method, or —
076    * at a higher protocol layer — a side-effect-free operation).
077    *
078    * @param t The throwable to inspect. Can be <jk>null</jk> (returns <jk>false</jk>).
079    * @return <jk>true</jk> if the throwable indicates a pre-response stale-connection failure.
080    */
081   public static boolean isStaleConnectionFailure(Throwable t) {
082      for (var t2 = t; t2 != null; t2 = t2.getCause()) {
083         if (t2 instanceof EOFException || t2 instanceof ClosedChannelException)
084            return true;
085         var msg = t2.getMessage();
086         if (msg != null) {
087            var m = msg.toLowerCase(Locale.ROOT);
088            if (m.contains("received no bytes") || (t2 instanceof SocketException && m.contains("reset")))
089               return true;
090         }
091      }
092      return false;
093   }
094}