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.server;
018
019import java.io.*;
020import java.nio.channels.*;
021import java.util.*;
022
023/**
024 * Internal utility for recognizing failures caused by the client disconnecting before the response completed.
025 *
026 * <p>
027 * A client that closes its connection mid-response (closed browser tab, cancelled request, timed-out proxy) is routine,
028 * not a server error.  Servlet containers surface it as an {@link IOException} from the response output stream or
029 * writer, but the concrete type is container-specific.  This class detects it without a compile-time dependency on any
030 * container.
031 *
032 * <h5 class='section'>Example:</h5>
033 * <p class='bjava'>
034 *    <jk>try</jk> {
035 *       <jv>res</jv>.flushBuffer();
036 *    } <jk>catch</jk> (Exception <jv>e</jv>) {
037 *       <jk>if</jk> (ClientAborts.<jsm>isClientAbort</jsm>(<jv>e</jv>))
038 *          <jc>// Routine: the client went away.  Don't record it as an error.</jc>
039 *          <jk>return</jk>;
040 *       <jk>throw</jk> <jv>e</jv>;
041 *    }
042 * </p>
043 *
044 * <p>
045 * Not part of the public REST API; subject to change without notice.
046 *
047 * @since 10.0.0
048 */
049public final class ClientAborts {
050
051   private static final int MAX_DEPTH = 32;
052
053   private ClientAborts() {}
054
055   /**
056    * Returns <jk>true</jk> if the specified throwable, or anything in its cause chain, indicates that the client
057    * disconnected.
058    *
059    * <p>
060    * The cause chain is walked up to a bounded depth and guarded against cycles.  A throwable matches if it is:
061    * <ul>
062    *    <li>A class whose simple name is <c>ClientAbortException</c> (Tomcat; matched by name so there is no Tomcat dependency).
063    *    <li>An {@link EOFException} (Jetty's <c>EofException</c> extends it).
064    *    <li>A {@link ClosedChannelException}.
065    *    <li>An {@link IOException} whose message contains <js>"broken pipe"</js> or <js>"connection reset"</js> (case-insensitive).
066    * </ul>
067    *
068    * @param t The throwable to test.  Can be <jk>null</jk>.
069    * @return <jk>true</jk> if the throwable represents a client abort.
070    */
071   public static boolean isClientAbort(Throwable t) {
072      return isClientAbort(t, false);
073   }
074
075   /**
076    * Returns <jk>true</jk> only if the throwable, or anything in its cause chain, is a servlet-container client-abort type.
077    *
078    * <p>
079    * Matches only classes whose simple name is <c>ClientAbortException</c> (Tomcat, which wraps every response write/flush
080    * failure in it) or <c>EofException</c> (Jetty).  Unlike {@link #isClientAbort(Throwable)}, plain {@link EOFException},
081    * {@link ClosedChannelException} and "broken pipe"/"connection reset" {@link IOException}s do <b>not</b> match.
082    *
083    * <p>
084    * Use this where the failure could have come from somewhere other than the client socket (e.g. an exception escaping
085    * operation execution or response rendering, where a backend call might also have reset).  Use the lenient
086    * {@link #isClientAbort(Throwable)} only for response flush paths, where the failure is necessarily on the client socket.
087    *
088    * @param t The throwable to test.  Can be <jk>null</jk>.
089    * @return <jk>true</jk> if the throwable represents a container-reported client abort.
090    */
091   public static boolean isContainerClientAbort(Throwable t) {
092      return isClientAbort(t, true);
093   }
094
095   private static boolean isClientAbort(Throwable t, boolean strict) {
096      var seen = Collections.newSetFromMap(new IdentityHashMap<Throwable,Boolean>());
097      for (var i = 0; t != null && i < MAX_DEPTH && seen.add(t); i++, t = t.getCause()) {
098         var n = t.getClass().getSimpleName();
099         if ("ClientAbortException".equals(n) || (strict && "EofException".equals(n)))
100            return true;
101         if (strict)
102            continue;
103         if (t instanceof EOFException || t instanceof ClosedChannelException)
104            return true;
105         if (t instanceof IOException && isAbortMessage(t.getMessage()))
106            return true;
107      }
108      return false;
109   }
110
111   private static boolean isAbortMessage(String m) {
112      if (m == null)
113         return false;
114      var lc = m.toLowerCase(Locale.ROOT);
115      return lc.contains("broken pipe") || lc.contains("connection reset");
116   }
117}