From e39e43a3809775c41a223ffa679e4f9e693676e1 Mon Sep 17 00:00:00 2001 From: stephentoub Date: Fri, 19 Feb 2016 14:08:52 -0500 Subject: [PATCH 1/2] Change CurlHandler to use IOExceptions for Stream failures As with WinHttpHandler, CurlHandler wraps exceptions in HttpRequestException. But WinHttpHandler switches to using IOException when sending exceptions out via Stream.Read/WriteAsync. This change does the same for CurlHandler. --- .../Net/Http/Unix/CurlHandler.CurlResponseMessage.cs | 4 ++-- .../Http/Unix/CurlHandler.HttpContentAsyncStream.cs | 4 ++-- .../src/System/Net/Http/Unix/CurlHandler.cs | 12 ++++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.CurlResponseMessage.cs b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.CurlResponseMessage.cs index 422a60f42213..4d8280e314e9 100644 --- a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.CurlResponseMessage.cs +++ b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.CurlResponseMessage.cs @@ -268,7 +268,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel OperationCanceledException oce = _completed as OperationCanceledException; return (oce != null && oce.CancellationToken.IsCancellationRequested) ? Task.FromCanceled(oce.CancellationToken) : - Task.FromException(_completed); + Task.FromException(MapToReadWriteIOException(_completed, isRead: true)); } // Quick check for if no data was actually requested. We do this after the check @@ -380,7 +380,7 @@ internal void SignalComplete(Exception error = null) } else { - _pendingReadRequest.TrySetException(_completed); + _pendingReadRequest.TrySetException(MapToReadWriteIOException(_completed, isRead: true)); } } diff --git a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.HttpContentAsyncStream.cs b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.HttpContentAsyncStream.cs index 3817a559fc67..e692b8d59b9f 100644 --- a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.HttpContentAsyncStream.cs +++ b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.HttpContentAsyncStream.cs @@ -180,7 +180,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati // If we've been disposed, throw an exception so as to end the CopyToAsync operation. if (_disposed) { - throw CreateHttpRequestException(); + throw new ObjectDisposedException(GetType().FullName); } if (_buffer == null) @@ -349,7 +349,7 @@ private void EndCopyToAsync(Task completedCopy) Debug.Assert(_waiterIsReader, "We're done writing, so a waiter must be a reader"); if (completedCopy.IsFaulted) { - _asyncOp.TrySetException(completedCopy.Exception.InnerException); + _asyncOp.TrySetException(MapToReadWriteIOException(completedCopy.Exception.InnerException, isRead: _waiterIsReader)); _copyTask = completedCopy; } else if (completedCopy.IsCanceled) diff --git a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs index 2f7d4d09a2b5..916b376271aa 100644 --- a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs +++ b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using System.Collections.Generic; using CURLAUTH = Interop.Http.CURLAUTH; using CURLcode = Interop.Http.CURLcode; @@ -625,11 +626,18 @@ private static void EventSourceTraceCore(string message, MultiAgent agent, EasyR message); } - private static Exception CreateHttpRequestException(Exception inner = null) + private static HttpRequestException CreateHttpRequestException(Exception inner = null) { return new HttpRequestException(SR.net_http_client_execution_error, inner); } + private static IOException MapToReadWriteIOException(Exception error, bool isRead) + { + return new IOException( + isRead ? SR.net_http_io_read : SR.net_http_io_write, + error is HttpRequestException && error.InnerException != null ? error.InnerException : error); + } + private static bool TryParseStatusLine(HttpResponseMessage response, string responseHeader, EasyRequest state) { if (!responseHeader.StartsWith(CurlResponseParseUtils.HttpPrefix, StringComparison.OrdinalIgnoreCase)) From f9d488485d2bb7edeb43f918af1746d9ecb5e758 Mon Sep 17 00:00:00 2001 From: stephentoub Date: Fri, 19 Feb 2016 14:55:14 -0500 Subject: [PATCH 2/2] Add test for invalid response content length --- .../FunctionalTests/ResponseStreamTest.cs | 51 ++++++++++++++++++- .../tests/FunctionalTests/project.json | 1 + 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/System.Net.Http/tests/FunctionalTests/ResponseStreamTest.cs b/src/System.Net.Http/tests/FunctionalTests/ResponseStreamTest.cs index 5c94e3e1875c..473f5891857b 100644 --- a/src/System.Net.Http/tests/FunctionalTests/ResponseStreamTest.cs +++ b/src/System.Net.Http/tests/FunctionalTests/ResponseStreamTest.cs @@ -2,9 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.IO; +using System.Net.Sockets; using System.Net.Test.Common; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -90,6 +91,54 @@ await client.GetAsync(HttpTestServers.RemoteEchoServer, HttpCompletionOption.Res } } + [ActiveIssue(6231, PlatformID.Windows)] + [Fact] + public async Task ReadAsStreamAsync_IncompleteContentLengthResponse_ThrowsIOException() + { + var server = new TcpListener(IPAddress.Loopback, 0); + Task serverTask = ((Func)async delegate { + server.Start(); + using (var client = await server.AcceptSocketAsync()) + using (var stream = new NetworkStream(client)) + using (var reader = new StreamReader(stream, Encoding.ASCII)) + { + // Read request + string line; + while (!string.IsNullOrEmpty(line = reader.ReadLine())) ; + + // Write response, with a potentially invalid content length + using (var writer = new StreamWriter(stream, Encoding.ASCII)) + { + string content = "This is some response content."; + writer.Write("HTTP/1.1 200 OK\r\n"); + writer.Write($"Date: {DateTimeOffset.UtcNow:R}\r\n"); + writer.Write("Content-Type: text/plain\r\n"); + writer.Write($"Content-Length: {content.Length + 42}\r\n"); // incorrect length + writer.Write("\r\n"); + writer.Write(content); + writer.Flush(); + } + + client.Shutdown(SocketShutdown.Both); + } + })(); + + using (var client = new HttpClient()) + { + var local = (IPEndPoint)server.LocalEndpoint; + using (var response = await client.GetAsync(new Uri($"http://localhost:{((IPEndPoint)server.LocalEndpoint).Port}/"), HttpCompletionOption.ResponseHeadersRead)) + using (var stream = await response.Content.ReadAsStreamAsync()) + { + await Assert.ThrowsAsync(async () => { + var buffer = new byte[1]; + while (await stream.ReadAsync(buffer, 0, 1) > 0) ; + }); + } + } + + await serverTask; + } + // These methods help to validate the response body since the endpoint will echo // all request headers. // diff --git a/src/System.Net.Http/tests/FunctionalTests/project.json b/src/System.Net.Http/tests/FunctionalTests/project.json index a4346c3f8d62..f34002ea3727 100644 --- a/src/System.Net.Http/tests/FunctionalTests/project.json +++ b/src/System.Net.Http/tests/FunctionalTests/project.json @@ -7,6 +7,7 @@ "System.IO": "4.0.10", "System.IO.FileSystem": "4.0.0", "System.Net.Primitives": "4.0.11-rc3-23818", + "System.Net.Sockets": "4.1.0-rc3-23818", "System.Runtime.Extensions": "4.0.10", "System.Security.Cryptography.Algorithms": "4.0.0-rc3-23818", "System.Text.Encoding": "4.0.10",