Skip to content

Commit

Permalink
Make HttpResponseMessage.Content non-nullable (#35910)
Browse files Browse the repository at this point in the history
* Make HttpResponseMessage.Content non-nullable

* Fix Microsoft.Extensions.Http test
  • Loading branch information
stephentoub committed May 8, 2020
1 parent 43b5d81 commit b48900f
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { } }
Expand Down
7 changes: 4 additions & 3 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<AssemblyName>System.Net.Http</AssemblyName>
Expand All @@ -18,12 +18,15 @@
<Compile Include="System\Net\Http\ByteArrayHelpers.cs" />
<Compile Include="System\Net\Http\ClientCertificateOption.cs" />
<Compile Include="System\Net\Http\DelegatingHandler.cs" />
<Compile Include="System\Net\Http\EmptyContent.cs" />
<Compile Include="System\Net\Http\EmptyReadStream.cs" />
<Compile Include="System\Net\Http\FormUrlEncodedContent.cs" />
<Compile Include="System\Net\Http\Headers\AltSvcHeaderParser.cs" />
<Compile Include="System\Net\Http\Headers\AltSvcHeaderValue.cs" />
<Compile Include="System\Net\Http\Headers\KnownHeader.cs" />
<Compile Include="System\Net\Http\Headers\HttpHeaderType.cs" />
<Compile Include="System\Net\Http\Headers\KnownHeaders.cs" />
<Compile Include="System\Net\Http\HttpBaseStream.cs" />
<Compile Include="System\Net\Http\HttpClient.cs" />
<Compile Include="System\Net\Http\HttpClientHandler.cs" />
<Compile Include="System\Net\Http\HttpClientHandler.Core.cs" />
Expand Down Expand Up @@ -128,7 +131,6 @@
<Compile Include="System\Net\Http\SocketsHttpHandler\CreditManager.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\CreditWaiter.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\DecompressionHandler.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\EmptyReadStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\FailedProxyCache.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2Connection.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ConnectionException.cs" />
Expand All @@ -142,7 +144,6 @@
<Compile Include="System\Net\Http\SocketsHttpHandler\Http3RequestStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpAuthenticatedConnectionHandler.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpAuthority.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpBaseStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnection.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionBase.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionHandler.cs" />
Expand Down
38 changes: 38 additions & 0 deletions src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>Provides a zero-length HttpContent implementation.</summary>
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<Stream> CreateContentReadStreamAsync() =>
Task.FromResult<Stream>(EmptyReadStream.Instance);

protected override Task<Stream> CreateContentReadStreamAsync(CancellationToken cancellationToken) =>
cancellationToken.IsCancellationRequested ? Task.FromCanceled<Stream>(cancellationToken) :
CreateContentReadStreamAsync();

internal override Stream? TryCreateContentReadStream() => EmptyReadStream.Instance;

internal override bool AllowDuplex => false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ public async Task GetContentAsync_NullResponseContent_ReturnsDefaultValue()
{
Assert.Same(string.Empty, await client.GetStringAsync(CreateFakeUri()));
Assert.Same(Array.Empty<byte>(), 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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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<Stream> 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<byte>(new byte[1])));

Assert.Throws<NotSupportedException>(() => s.WriteByte(0));
Assert.Throws<NotSupportedException>(() => s.Write(new byte[1], 0, 1));
await Assert.ThrowsAsync<NotSupportedException>(() => s.WriteAsync(new byte[1], 0, 1));
await Assert.ThrowsAsync<NotSupportedException>(async () => await s.WriteAsync(new ReadOnlyMemory<byte>(new byte[1])));
}

[Fact]
public void ToString_DefaultAndNonDefaultInstance_DumpAllFields()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public static class ClientOperations
using HttpResponseMessage m = await ctx.SendAsync(req);
ValidateStatusCode(m);
ValidateServerContent(await m.Content!.ReadAsStringAsync(), expectedLength);
ValidateServerContent(await m.Content.ReadAsStringAsync(), expectedLength);
}),

("GET Partial",
Expand All @@ -204,7 +204,7 @@ public static class ClientOperations
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
}
Expand All @@ -221,7 +221,7 @@ public static class ClientOperations
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";
Expand Down Expand Up @@ -273,7 +273,7 @@ public static class ClientOperations
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",
Expand Down Expand Up @@ -332,7 +332,7 @@ public static class ClientOperations
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",
Expand All @@ -346,7 +346,7 @@ public static class ClientOperations
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",
Expand All @@ -359,7 +359,7 @@ public static class ClientOperations
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);
Expand All @@ -376,7 +376,7 @@ public static class ClientOperations
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);
Expand Down Expand Up @@ -416,7 +416,7 @@ public static class ClientOperations
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",
Expand All @@ -428,7 +428,7 @@ public static class ClientOperations
ValidateStatusCode(m);
if (m.Content!.Headers.ContentLength != expectedLength)
if (m.Content.Headers.ContentLength != expectedLength)
{
throw new Exception($"Expected {expectedLength}, got {m.Content.Headers.ContentLength}");
}
Expand All @@ -446,7 +446,7 @@ public static class ClientOperations
ValidateStatusCode(m);
string r = await m.Content!.ReadAsStringAsync();
string r = await m.Content.ReadAsStringAsync();
if (r != "") throw new Exception($"Got unexpected response: {r}");
}),

Expand All @@ -460,7 +460,7 @@ public static class ClientOperations
ValidateStatusCode(m);
string r = await m.Content!.ReadAsStringAsync();
string r = await m.Content.ReadAsStringAsync();
if (r != "") throw new Exception($"Got unexpected response: {r}");
}),

Expand All @@ -472,7 +472,7 @@ public static class ClientOperations
using HttpResponseMessage m = await ctx.SendAsync(req);
ValidateStatusCode(m);
ValidateServerContent(await m.Content!.ReadAsStringAsync(), expectedLength);
ValidateServerContent(await m.Content.ReadAsStringAsync(), expectedLength);
}),
};

Expand Down
Loading

0 comments on commit b48900f

Please sign in to comment.