diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Logging/RedactedLogValueIntegrationTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Logging/RedactedLogValueIntegrationTest.cs index 67ec29684936e..3b3c122b52fa9 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Logging/RedactedLogValueIntegrationTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/Logging/RedactedLogValueIntegrationTest.cs @@ -51,7 +51,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging() m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.RequestHeader && m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Request Headers: Authorization: * Cache-Control: no-cache @@ -63,7 +63,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging() m.EventId == LoggingHttpMessageHandler.Log.EventIds.RequestHeader && m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Request Headers: Authorization: * Cache-Control: no-cache @@ -75,7 +75,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging() m.EventId == LoggingHttpMessageHandler.Log.EventIds.ResponseHeader && m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Response Headers: X-Sensitive: * Y-Non-Sensitive: innocuous value @@ -87,7 +87,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging() m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.ResponseHeader && m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Response Headers: X-Sensitive: * Y-Non-Sensitive: innocuous value @@ -132,7 +132,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging() m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.RequestHeader && m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Request Headers: Authorization: * Cache-Control: no-cache @@ -144,7 +144,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging() m.EventId == LoggingHttpMessageHandler.Log.EventIds.RequestHeader && m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Request Headers: Authorization: * Cache-Control: no-cache @@ -156,7 +156,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging() m.EventId == LoggingHttpMessageHandler.Log.EventIds.ResponseHeader && m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Response Headers: X-Sensitive: * Y-Non-Sensitive: innocuous value @@ -168,7 +168,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging() m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.ResponseHeader && m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler"; })); - Assert.Equal( + Assert.StartsWith( @"Response Headers: X-Sensitive: * Y-Non-Sensitive: innocuous value diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 0e0752189fe65..3026fa779ab0d 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -209,7 +209,8 @@ public partial class HttpResponseMessage : System.IDisposable { public HttpResponseMessage() { } public HttpResponseMessage(System.Net.HttpStatusCode statusCode) { } - public System.Net.Http.HttpContent? Content { get { throw null; } set { } } + [System.Diagnostics.CodeAnalysis.AllowNull] + public System.Net.Http.HttpContent Content { get { throw null; } set { } } public System.Net.Http.Headers.HttpResponseHeaders Headers { get { throw null; } } public bool IsSuccessStatusCode { get { throw null; } } public string? ReasonPhrase { get { throw null; } set { } } diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index dbdc3aa728d74..c4f1487f71f88 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -1,4 +1,4 @@ - + Library System.Net.Http @@ -18,12 +18,15 @@ + + + @@ -127,7 +130,6 @@ - @@ -141,7 +143,6 @@ - diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs new file mode 100644 index 0000000000000..728229337c675 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + /// Provides a zero-length HttpContent implementation. + internal sealed class EmptyContent : HttpContent + { + protected internal override bool TryComputeLength(out long length) + { + length = 0; + return true; + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + Task.CompletedTask; + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : + SerializeToStreamAsync(stream, context); + + protected override Task CreateContentReadStreamAsync() => + Task.FromResult(EmptyReadStream.Instance); + + protected override Task CreateContentReadStreamAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : + CreateContentReadStreamAsync(); + + internal override Stream? TryCreateContentReadStream() => EmptyReadStream.Instance; + + internal override bool AllowDuplex => false; + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/EmptyReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/EmptyReadStream.cs similarity index 100% rename from src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/EmptyReadStream.cs rename to src/libraries/System.Net.Http/src/System/Net/Http/EmptyReadStream.cs diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpBaseStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs similarity index 100% rename from src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpBaseStream.cs rename to src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs index dbe85b55dea63..b0763db9560bb 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs @@ -2,6 +2,7 @@ // 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.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Text; @@ -39,9 +40,10 @@ public Version Version internal void SetVersionWithoutValidation(Version value) => _version = value; - public HttpContent? Content + [AllowNull] + public HttpContent Content { - get { return _content; } + get { return _content ??= new EmptyContent(); } set { CheckDisposed(); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index a297e28766d3a..544f188bd5770 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -221,7 +221,10 @@ public async Task GetContentAsync_NullResponseContent_ReturnsDefaultValue() { Assert.Same(string.Empty, await client.GetStringAsync(CreateFakeUri())); Assert.Same(Array.Empty(), await client.GetByteArrayAsync(CreateFakeUri())); - Assert.Same(Stream.Null, await client.GetStreamAsync(CreateFakeUri())); + + Stream s = await client.GetStreamAsync(CreateFakeUri()); + Assert.NotNull(s); + Assert.Equal(-1, s.ReadByte()); } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpResponseMessageTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpResponseMessageTest.cs index fdeeeb065ca23..7c7fafee8d348 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpResponseMessageTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpResponseMessageTest.cs @@ -2,10 +2,7 @@ // 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.Http.Headers; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -22,7 +19,7 @@ public void Ctor_Default_CorrectDefaults() Assert.Equal(HttpStatusCode.OK, rm.StatusCode); Assert.Equal("OK", rm.ReasonPhrase); Assert.Equal(new Version(1, 1), rm.Version); - Assert.Null(rm.Content); + Assert.NotNull(rm.Content); Assert.Null(rm.RequestMessage); } } @@ -35,7 +32,7 @@ public void Ctor_SpecifiedValues_CorrectValues() Assert.Equal(HttpStatusCode.Accepted, rm.StatusCode); Assert.Equal("Accepted", rm.ReasonPhrase); Assert.Equal(new Version(1, 1), rm.Version); - Assert.Null(rm.Content); + Assert.NotNull(rm.Content); Assert.Null(rm.RequestMessage); } } @@ -232,8 +229,15 @@ public void Content_SetToNull_Accepted() { using (var rm = new HttpResponseMessage()) { + HttpContent c1 = rm.Content; + Assert.Same(c1, rm.Content); + rm.Content = null; - Assert.Null(rm.Content); + + HttpContent c2 = rm.Content; + Assert.Same(c2, rm.Content); + + Assert.NotSame(c1, c2); } } @@ -249,6 +253,35 @@ public void StatusCode_InvalidStatusCodeRange_ThrowsArgumentOutOfRangeException( } } + [Fact] + public async Task DefaultContent_ReadableNotWritable_Success() + { + var resp = new HttpResponseMessage(); + + HttpContent c = resp.Content; + Assert.NotNull(c); + Assert.Same(c, resp.Content); + Assert.NotSame(resp.Content, new HttpResponseMessage().Content); + + Assert.Equal(0, c.Headers.ContentLength); + + Task t = c.ReadAsStreamAsync(); + Assert.Equal(TaskStatus.RanToCompletion, t.Status); + + Stream s = await t; + Assert.NotNull(s); + + Assert.Equal(-1, s.ReadByte()); + Assert.Equal(0, s.Read(new byte[1], 0, 1)); + Assert.Equal(0, await s.ReadAsync(new byte[1], 0, 1)); + Assert.Equal(0, await s.ReadAsync(new Memory(new byte[1]))); + + Assert.Throws(() => s.WriteByte(0)); + Assert.Throws(() => s.Write(new byte[1], 0, 1)); + await Assert.ThrowsAsync(() => s.WriteAsync(new byte[1], 0, 1)); + await Assert.ThrowsAsync(async () => await s.WriteAsync(new ReadOnlyMemory(new byte[1]))); + } + [Fact] public void ToString_DefaultAndNonDefaultInstance_DumpAllFields() { diff --git a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs index 2e148dd757e33..8342fed20b694 100644 --- a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs +++ b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs @@ -192,7 +192,7 @@ public static (string name, Func operation)[] Operations = using HttpResponseMessage m = await ctx.SendAsync(req); ValidateStatusCode(m); - ValidateServerContent(await m.Content!.ReadAsStringAsync(), expectedLength); + ValidateServerContent(await m.Content.ReadAsStringAsync(), expectedLength); }), ("GET Partial", @@ -204,7 +204,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(m); - using (Stream s = await m.Content!.ReadAsStreamAsync()) + using (Stream s = await m.Content.ReadAsStreamAsync()) { s.ReadByte(); // read single byte from response and throw the rest away } @@ -221,7 +221,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(res); - await res.Content!.ReadAsStringAsync(); + await res.Content.ReadAsStringAsync(); bool isValidChecksum = ValidateServerChecksum(res.Headers, expectedChecksum); string failureDetails = isValidChecksum ? "server checksum matches client checksum" : "server checksum mismatch"; @@ -273,7 +273,7 @@ public static (string name, Func operation)[] Operations = using HttpResponseMessage m = await ctx.SendAsync(req); ValidateStatusCode(m); - ValidateContent(expectedResponse, await m.Content!.ReadAsStringAsync(), $"Uri: {uri}"); + ValidateContent(expectedResponse, await m.Content.ReadAsStringAsync(), $"Uri: {uri}"); }), ("GET Aborted", @@ -332,7 +332,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(m); string checksumMessage = ValidateServerChecksum(m.Headers, checksum) ? "server checksum matches client checksum" : "server checksum mismatch"; - ValidateContent(content, await m.Content!.ReadAsStringAsync(), checksumMessage); + ValidateContent(content, await m.Content.ReadAsStringAsync(), checksumMessage); }), ("POST Multipart Data", @@ -346,7 +346,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(m); string checksumMessage = ValidateServerChecksum(m.Headers, checksum) ? "server checksum matches client checksum" : "server checksum mismatch"; - ValidateContent(formData.expected, await m.Content!.ReadAsStringAsync(), checksumMessage); + ValidateContent(formData.expected, await m.Content.ReadAsStringAsync(), checksumMessage); }), ("POST Duplex", @@ -359,7 +359,7 @@ public static (string name, Func operation)[] Operations = using HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); ValidateStatusCode(m); - string response = await m.Content!.ReadAsStringAsync(); + string response = await m.Content.ReadAsStringAsync(); string checksumMessage = ValidateServerChecksum(m.TrailingHeaders, checksum, required: false) ? "server checksum matches client checksum" : "server checksum mismatch"; ValidateContent(content, await m.Content.ReadAsStringAsync(), checksumMessage); @@ -376,7 +376,7 @@ public static (string name, Func operation)[] Operations = using HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); ValidateStatusCode(m); - string response = await m.Content!.ReadAsStringAsync(); + string response = await m.Content.ReadAsStringAsync(); // trailing headers not supported for all servers, so do not require checksums bool isValidChecksum = ValidateServerChecksum(m.TrailingHeaders, checksum, required: false); @@ -416,7 +416,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(m); string checksumMessage = ValidateServerChecksum(m.Headers, checksum) ? "server checksum matches client checksum" : "server checksum mismatch"; - ValidateContent(content, await m.Content!.ReadAsStringAsync(), checksumMessage); + ValidateContent(content, await m.Content.ReadAsStringAsync(), checksumMessage); }), ("HEAD", @@ -428,7 +428,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(m); - if (m.Content!.Headers.ContentLength != expectedLength) + if (m.Content.Headers.ContentLength != expectedLength) { throw new Exception($"Expected {expectedLength}, got {m.Content.Headers.ContentLength}"); } @@ -446,7 +446,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(m); - string r = await m.Content!.ReadAsStringAsync(); + string r = await m.Content.ReadAsStringAsync(); if (r != "") throw new Exception($"Got unexpected response: {r}"); }), @@ -460,7 +460,7 @@ public static (string name, Func operation)[] Operations = ValidateStatusCode(m); - string r = await m.Content!.ReadAsStringAsync(); + string r = await m.Content.ReadAsStringAsync(); if (r != "") throw new Exception($"Got unexpected response: {r}"); }), @@ -472,7 +472,7 @@ public static (string name, Func operation)[] Operations = using HttpResponseMessage m = await ctx.SendAsync(req); ValidateStatusCode(m); - ValidateServerContent(await m.Content!.ReadAsStringAsync(), expectedLength); + ValidateServerContent(await m.Content.ReadAsStringAsync(), expectedLength); }), }; diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index c14ef4c1dd220..b4c31ba80fd7b 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -70,6 +70,8 @@ Link="ProductionCode\Common\System\Threading\Tasks\TaskToApm.cs" /> + + +