Search code examples
wcfhttpwebrequestthreadpoolkeep-alivesystem.net.webexception

Request canceled exception under HttpWebRequest.Abort


My application is querying my server every few seconds for updates.

After leaving it running for about 3 days I observed that the app crashed with the following stack trace.

As you may know, when getting an exception in a working thread, it can't be caught, hence my app crashed.

System.Net.WebException: The request was canceled
System.Net.ServicePointManager.FindServicePoint(Uri address, IWebProxy proxy, ProxyChain& chain, HttpAbortDelegate& abortDelegate, Int32& abortState)
System.Net.HttpWebRequest.FindServicePoint(Boolean forceFind)
System.Net.AuthenticationState.PrepareState(HttpWebRequest httpWebRequest)
System.Net.AuthenticationState.ClearSession(HttpWebRequest httpWebRequest)
System.Net.HttpWebRequest.ClearAuthenticatedConnectionResources()
System.Net.HttpWebRequest.Abort(Exception exception, Int32 abortState)
System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
System.Threading.ThreadPoolWorkQueue.Dispatch()

I've seen many similar threads around the web. But all who had the same stack did not receive any help.

I've also seen that many recommended to set the property of my HttpWebRequest with KeepAlive=false, however, that may hurt my performance and is unacceptable.


Solution

  • This is, in fact, a known bug in Microsoft's framework, ad mentioned here:

    https://support.microsoft.com/en-us/kb/2750147

    The weird issue about this is that my application is running with .NET4.0 and not .NET4.5

    After talking with Microsoft's Support, it seems that if .NET4.5 is installed on the machine, then the application's behavior is changed. So it in fact does make sense.

    The evidence could be found in MS's source code. From http://referencesource.microsoft.com/#q=httpwebrequest :

            // TimeoutCallback - Called by the TimerThread to abort a request.  This just posts ThreadPool work item - Abort() does too
            // much to be done on the timer thread (timer thread should never block or call user code).
            private static void TimeoutCallback(TimerThread.Timer timer, int timeNoticed, object context)
            {
                ThreadPool.UnsafeQueueUserWorkItem(s_AbortWrapper, context);
            }
    
            private void Abort(Exception exception, int abortState)
            {
                GlobalLog.ThreadContract(ThreadKinds.Unknown, "HttpWebRequest#" + ValidationHelper.HashString(this) + "::Abort()");
                if (Logging.On) Logging.Enter(Logging.Web, this, "Abort", (exception == null? "" :  exception.Message));
    
                if(Interlocked.CompareExchange(ref m_Aborted, abortState, 0) == 0) // public abort will never drain streams
                {
                    GlobalLog.Print("HttpWebRequest#" + ValidationHelper.HashString(this) + "::Abort() - " + exception);
    
                    NetworkingPerfCounters.Instance.Increment(NetworkingPerfCounterName.HttpWebRequestAborted);
    
                    m_OnceFailed = true;
                    CancelTimer();
    
                    WebException webException = exception as WebException;
                    if (exception == null)
                    {
                        webException = new WebException(NetRes.GetWebStatusString("net_requestaborted", WebExceptionStatus.RequestCanceled), WebExceptionStatus.RequestCanceled);
                    }
                    else if (webException == null)
                    {
                        webException = new WebException(NetRes.GetWebStatusString("net_requestaborted", WebExceptionStatus.RequestCanceled), exception, WebExceptionStatus.RequestCanceled, _HttpResponse);
                    }
    
                    try
                    {
    #if DEBUG
                        bool setResponseCalled = false;
                        try
                        {
    #endif
                            // Want to make sure that other threads see that we're aborted before they set an abort delegate, or that we see
                            // the delegate if they might have missed that we're aborted.
                            Thread.MemoryBarrier();
                            HttpAbortDelegate abortDelegate = _AbortDelegate;
    #if DEBUG
                            m_AbortDelegateUsed = abortDelegate == null ? (object)DBNull.Value : abortDelegate;
    #endif
                            if (abortDelegate == null || abortDelegate(this, webException))
                            {
                                // We don't have a connection associated with this request
    
    #if DEBUG
                                setResponseCalled = true;
    #endif
                                SetResponse(webException);
                            }
                            else
                            {
                                // In case we don't call SetResponse(), make sure to complete the lazy async result
                                // objects. abortDelegate() may not end up in a code path that would complete these
                                // objects.
                                LazyAsyncResult writeAResult = null;
                                LazyAsyncResult readAResult = null;
    
                                if (!Async)
                                {
                                    lock (this)
                                    {
                                        writeAResult = _WriteAResult;
                                        readAResult = _ReadAResult;
                                    }
                                }
    
                                if (writeAResult != null)
                                    writeAResult.InvokeCallback(webException);
    
                                if (readAResult != null)
                                    readAResult.InvokeCallback(webException);
                            }
    
                            if (!Async)
                            {
                                LazyAsyncResult chkConnectionAsyncResult = ConnectionAsyncResult;
                                LazyAsyncResult chkReaderAsyncResult = ConnectionReaderAsyncResult;
    
                                if (chkConnectionAsyncResult != null)
                                    chkConnectionAsyncResult.InvokeCallback(webException);
                                if (chkReaderAsyncResult != null)
                                    chkReaderAsyncResult.InvokeCallback(webException);
                            }
    
                            if (this.IsWebSocketRequest && this.ServicePoint != null)
                            {
                                this.ServicePoint.CloseConnectionGroup(this.ConnectionGroupName);
                            }
    #if DEBUG
                        }
                        catch (Exception stressException)
                        {
                            t_LastStressException = stressException;
                        if (!NclUtilities.IsFatal(stressException)){
                            GlobalLog.Assert(setResponseCalled, "HttpWebRequest#{0}::Abort|{1}", ValidationHelper.HashString(this), stressException.Message);
                            }
                            throw;
                        }
    #endif
                    }
                    catch (InternalException)
                    {
                    }
                }
    
                if(Logging.On)Logging.Exit(Logging.Web, this, "Abort", "");
            }
    

    As you can see, TimeoutCallback is calling the abort method in a new thread, meaning its not exception-proof.

    In addition, Abort may throw an exception in some scenarios. In theory, this could be easily reproduced.