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}