/// <summary>Read complete request body if not done by ReadRequestData.</summary>
public abstract Task<Byte[]> ReadRequestBodyAsync();
- /// <summary>Sends Response back with provided statusCode, headers and content. Can be called multiple times on same response if isFinal was set to false before.</summary>
- public abstract Task SendResponseAsync(HttpStatusCode? statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true, int requestId = 0);
+ /// <summary>Sends Response back with provided statusCode, headers and content.
+ /// If isFinal is false, the body is not completed and you can call SendResponseBodyAsync to send more.</summary>
+ public abstract Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true, int requestId = 0);
/// <summary>Sends response headers.</summary>
public abstract Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0);
+ /// <summary>Sends valid but incomplete headers. Once called, there is no way to continue the response past this point.</summary>
+ public abstract Task SendPartialResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0);
/// <summary>Sends Response body after SendResponse was called with isFinal: false.</summary>
public abstract Task SendResponseBodyAsync(byte[] content, bool isFinal = true, int requestId = 0);
public string Path;
public Version Version;
public List<HttpHeaderData> Headers { get; }
- public int RequestId; // Generic request ID. Currently only used for HTTP/2 to hold StreamId.
+ public int RequestId; // HTTP/2 StreamId.
public HttpRequestData()
{
{
return Headers.Where(h => h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase)).Count();
}
+
+ public override string ToString() => $"{Method} {Path} HTTP/{Version}\r\n{string.Join("\r\n", Headers)}\r\n\r\n";
}
}
return ReadBodyAsync();
}
- public override async Task SendResponseAsync(HttpStatusCode? statusCode = null, IList<HttpHeaderData> headers = null, string body = null, bool isFinal = true, int requestId = 0)
+ public override async Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true, int requestId = 0)
{
- // TODO: Header continuation support.
- Assert.NotNull(statusCode);
-
if (headers != null)
{
bool hasDate = false;
}
int streamId = requestId == 0 ? _lastStreamId : requestId;
- bool endHeaders = body != null || isFinal;
- if (string.IsNullOrEmpty(body))
+ if (string.IsNullOrEmpty(content))
{
- await SendResponseHeadersAsync(streamId, endStream: isFinal, (HttpStatusCode)statusCode, endHeaders: endHeaders, headers: headers);
+ await SendResponseHeadersAsync(streamId, endStream: isFinal, (HttpStatusCode)statusCode, endHeaders: true, headers: headers);
}
else
{
- await SendResponseHeadersAsync(streamId, endStream: false, (HttpStatusCode)statusCode, endHeaders: endHeaders, headers: headers);
- await SendResponseBodyAsync(body, isFinal: isFinal, requestId: streamId);
+ await SendResponseHeadersAsync(streamId, endStream: false, (HttpStatusCode)statusCode, endHeaders: true, headers: headers);
+ await SendResponseBodyAsync(content, isFinal: isFinal, requestId: streamId);
}
}
return SendResponseHeadersAsync(streamId, endStream: false, statusCode, isTrailingHeader: false, endHeaders: true, headers);
}
- public override Task SendResponseBodyAsync(byte[] body, bool isFinal = true, int requestId = 0)
+ public override Task SendPartialResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+ {
+ int streamId = requestId == 0 ? _lastStreamId : requestId;
+ return SendResponseHeadersAsync(streamId, endStream: false, statusCode, isTrailingHeader: false, endHeaders: false, headers);
+ }
+
+ public override Task SendResponseBodyAsync(byte[] content, bool isFinal = true, int requestId = 0)
{
int streamId = requestId == 0 ? _lastStreamId : requestId;
- return SendResponseBodyAsync(streamId, body, isFinal);
+ return SendResponseBodyAsync(streamId, content, isFinal);
}
public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "")
private readonly QuicConnection _connection;
private readonly Dictionary<int, Http3LoopbackStream> _openStreams = new Dictionary<int, Http3LoopbackStream>();
+ private Http3LoopbackStream _controlStream; // Our outbound control stream
private Http3LoopbackStream _currentStream;
private bool _closed;
}
}
+ public async Task EstablishControlStreamAsync()
+ {
+ _controlStream = OpenUnidirectionalStream();
+ await _controlStream.SendUnidirectionalStreamTypeAsync(Http3LoopbackStream.ControlStream);
+ await _controlStream.SendSettingsFrameAsync();
+ }
+
public override async Task<byte[]> ReadRequestBodyAsync()
{
return await _currentStream.ReadRequestBodyAsync().ConfigureAwait(false);
return await stream.ReadRequestDataAsync(readBody).ConfigureAwait(false);
}
- public override Task SendResponseAsync(HttpStatusCode? statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true, int requestId = 0)
+ public override Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true, int requestId = 0)
{
return GetOpenRequest(requestId).SendResponseAsync(statusCode, headers, content, isFinal);
}
return GetOpenRequest(requestId).SendResponseHeadersAsync(statusCode, headers);
}
+ public override Task SendPartialResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+ {
+ return GetOpenRequest(requestId).SendPartialResponseHeadersAsync(statusCode, headers);
+ }
+
public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "")
{
Http3LoopbackStream stream = await AcceptRequestStreamAsync().ConfigureAwait(false);
- HttpRequestData request = await stream.HandleRequestAsync(statusCode, headers, content);
+
+ HttpRequestData request = await stream.ReadRequestDataAsync().ConfigureAwait(false);
+
+ // We are about to close the connection, after we send the response.
+ // So, send a GOAWAY frame now so the client won't inadvertantly try to reuse the connection.
+ await _controlStream.SendGoAwayFrameAsync(stream.StreamId + 4);
+
+ await stream.SendResponseAsync(statusCode, headers, content).ConfigureAwait(false);
// closing the connection here causes bytes written to streams to go missing.
+ // Regardless, we told the client we are closing so it shouldn't matter -- they should not use this connection anymore.
//await CloseAsync(H3_NO_ERROR).ConfigureAwait(false);
return request;
public override async Task<GenericLoopbackConnection> EstablishGenericConnectionAsync()
{
QuicConnection con = await _listener.AcceptConnectionAsync().ConfigureAwait(false);
- return new Http3LoopbackConnection(con);
+ Http3LoopbackConnection connection = new Http3LoopbackConnection(con);
+
+ await connection.EstablishControlStreamAsync();
+ return connection;
}
public override async Task AcceptConnectionAsync(Func<GenericLoopbackConnection, Task> funcAsync)
private const long DataFrame = 0x0;
private const long HeadersFrame = 0x1;
private const long SettingsFrame = 0x4;
+ private const long GoAwayFrame = 0x7;
public const long ControlStream = 0x0;
public const long PushStream = 0x1;
{
_stream.Dispose();
}
+
+ public long StreamId => _stream.StreamId;
+
public async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "")
{
HttpRequestData request = await ReadRequestDataAsync().ConfigureAwait(false);
await _stream.WriteAsync(buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);
}
- public async Task SendSettingsFrameAsync(ICollection<(long settingId, long settingValue)> settings)
+ public async Task SendSettingsFrameAsync(ICollection<(long settingId, long settingValue)> settings = null)
{
+ settings ??= Array.Empty<(long settingId, long settingValue)>();
+
var buffer = new byte[settings.Count * MaximumVarIntBytes * 2];
int bytesWritten = 0;
await SendFrameAsync(SettingsFrame, buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);
}
- public async Task SendHeadersFrameAsync(IEnumerable<HttpHeaderData> headers)
+ private Memory<byte> ConstructHeadersPayload(IEnumerable<HttpHeaderData> headers)
{
int bufferLength = QPackTestEncoder.MaxPrefixLength;
bytesWritten += QPackTestEncoder.EncodeHeader(buffer.AsSpan(bytesWritten), header.Name, header.Value, header.ValueEncoding, header.HuffmanEncoded ? QPackFlags.HuffmanEncode : QPackFlags.None);
}
- await SendFrameAsync(HeadersFrame, buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);
+ return buffer.AsMemory(0, bytesWritten);
+ }
+
+ private async Task SendHeadersFrameAsync(IEnumerable<HttpHeaderData> headers)
+ {
+ await SendFrameAsync(HeadersFrame, ConstructHeadersPayload(headers)).ConfigureAwait(false);
+ }
+
+ private async Task SendPartialHeadersFrameAsync(IEnumerable<HttpHeaderData> headers)
+ {
+ Memory<byte> payload = ConstructHeadersPayload(headers);
+
+ await SendFrameHeaderAsync(HeadersFrame, payload.Length);
+
+ // Slice off final byte so the payload is not complete
+ payload = payload.Slice(0, payload.Length - 1);
+
+ await _stream.WriteAsync(payload).ConfigureAwait(false);
}
public async Task SendDataFrameAsync(ReadOnlyMemory<byte> data)
await SendFrameAsync(DataFrame, data).ConfigureAwait(false);
}
- public async Task SendFrameAsync(long frameType, ReadOnlyMemory<byte> framePayload)
+ // Note that unlike HTTP2, the stream ID here indicates the *first invalid* stream.
+ public async Task SendGoAwayFrameAsync(long firstInvalidStreamId)
+ {
+ var buffer = new byte[QPackTestEncoder.MaxVarIntLength];
+ int bytesWritten = 0;
+
+ bytesWritten += EncodeHttpInteger(firstInvalidStreamId, buffer);
+ await SendFrameAsync(GoAwayFrame, buffer.AsMemory(0, bytesWritten));
+ }
+
+ private async Task SendFrameHeaderAsync(long frameType, int payloadLength)
{
var buffer = new byte[MaximumVarIntBytes * 2];
int bytesWritten = 0;
bytesWritten += EncodeHttpInteger(frameType, buffer.AsSpan(bytesWritten));
- bytesWritten += EncodeHttpInteger(framePayload.Length, buffer.AsSpan(bytesWritten));
+ bytesWritten += EncodeHttpInteger(payloadLength, buffer.AsSpan(bytesWritten));
await _stream.WriteAsync(buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);
+ }
+
+ public async Task SendFrameAsync(long frameType, ReadOnlyMemory<byte> framePayload)
+ {
+ await SendFrameHeaderAsync(frameType, framePayload.Length).ConfigureAwait(false);
await _stream.WriteAsync(framePayload).ConfigureAwait(false);
}
return requestData;
}
- public async Task SendResponseAsync(HttpStatusCode? statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true)
+ public async Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true)
{
IEnumerable<HttpHeaderData> newHeaders = headers ?? Enumerable.Empty<HttpHeaderData>();
await SendResponseBodyAsync(Encoding.UTF8.GetBytes(content ?? ""), isFinal).ConfigureAwait(false);
}
- public async Task SendResponseHeadersAsync(HttpStatusCode? statusCode = HttpStatusCode.OK, IEnumerable<HttpHeaderData> headers = null)
+ private IEnumerable<HttpHeaderData> PrepareHeaders(HttpStatusCode statusCode, IEnumerable<HttpHeaderData> headers)
{
headers ??= Enumerable.Empty<HttpHeaderData>();
// Some tests use Content-Length with a null value to indicate Content-Length should not be set.
headers = headers.Where(x => x.Name != "Content-Length" || x.Value != null);
- if (statusCode != null)
- {
- headers = headers.Prepend(new HttpHeaderData(":status", ((int)statusCode).ToString(CultureInfo.InvariantCulture)));
- }
+ headers = headers.Prepend(new HttpHeaderData(":status", ((int)statusCode).ToString(CultureInfo.InvariantCulture)));
+
+ return headers;
+ }
+ public async Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IEnumerable<HttpHeaderData> headers = null)
+ {
+ headers = PrepareHeaders(statusCode, headers);
await SendHeadersFrameAsync(headers).ConfigureAwait(false);
}
+ public async Task SendPartialResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IEnumerable<HttpHeaderData> headers = null)
+ {
+ headers = PrepareHeaders(statusCode, headers);
+ await SendPartialHeadersFrameAsync(headers).ConfigureAwait(false);
+ }
+
public async Task SendResponseBodyAsync(byte[] content, bool isFinal = true)
{
if (content?.Length != 0)
Task serverTask = server.AcceptConnectionAsync(async connection =>
{
await connection.ReadRequestDataAsync();
- await connection.SendResponseAsync(HttpStatusCode.OK, content: null, isFinal: false);
+ await connection.SendPartialResponseHeadersAsync(HttpStatusCode.OK);
partialResponseHeadersSent.TrySetResult(true);
await clientFinished.Task;
Task serverTask2 = server2.AcceptConnectionAsync(async connection2 =>
{
await connection2.ReadRequestDataAsync();
- await connection2.SendResponseAsync(HttpStatusCode.OK, content: null, isFinal : false);
+ await connection2.SendPartialResponseHeadersAsync(HttpStatusCode.OK);
await unblockServers.Task;
});
return buffer;
}
- public override async Task SendResponseAsync(HttpStatusCode? statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = null, bool isFinal = true, int requestId = 0)
+ public override async Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true, int requestId = 0)
{
MemoryStream headerBytes = new MemoryStream();
int contentLength = -1;
headerBytes.Write(corsBytes, 0, corsBytes.Length);
}
- bool endHeaders = content != null || isFinal;
- if (statusCode != null)
- {
- byte[] temp = headerBytes.ToArray();
+ byte[] temp = headerBytes.ToArray();
- headerBytes.SetLength(0);
+ headerBytes.SetLength(0);
- byte[] headerStartBytes = Encoding.ASCII.GetBytes(
- $"HTTP/1.1 {(int)statusCode} {GetStatusDescription((HttpStatusCode)statusCode)}\r\n" +
- (!hasContentLength && !isChunked && content != null ? $"Content-length: {content.Length}\r\n" : ""));
+ byte[] headerStartBytes = Encoding.ASCII.GetBytes(
+ $"HTTP/1.1 {(int)statusCode} {GetStatusDescription(statusCode)}\r\n" +
+ (!hasContentLength && !isChunked && content != null ? $"Content-length: {content.Length}\r\n" : ""));
- headerBytes.Write(headerStartBytes, 0, headerStartBytes.Length);
- headerBytes.Write(temp, 0, temp.Length);
+ headerBytes.Write(headerStartBytes, 0, headerStartBytes.Length);
+ headerBytes.Write(temp, 0, temp.Length);
- if (endHeaders)
- {
- headerBytes.Write(s_newLineBytes, 0, s_newLineBytes.Length);
- }
- }
+ headerBytes.Write(s_newLineBytes, 0, s_newLineBytes.Length);
headerBytes.Position = 0;
await headerBytes.CopyToAsync(_stream).ConfigureAwait(false);
}
}
- public override async Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+ private string GetResponseHeaderString(HttpStatusCode statusCode, IList<HttpHeaderData> headers)
{
string headerString = null;
headerString = GetHttpResponseHeaders(statusCode, headerString, 0, connectionClose: true);
+ return headerString;
+ }
+
+ public override async Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+ {
+ string headerString = GetResponseHeaderString(statusCode, headers);
+ await SendResponseAsync(headerString).ConfigureAwait(false);
+ }
+
+ public override async Task SendPartialResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+ {
+ string headerString = GetResponseHeaderString(statusCode, headers);
+
+ // Lop off the final \r\n so the headers are not complete.
+ headerString = headerString.Substring(0, headerString.Length - 2);
+
await SendResponseAsync(headerString).ConfigureAwait(false);
}
- public override async Task SendResponseBodyAsync(byte[] body, bool isFinal = true, int requestId = 0)
+ public override async Task SendResponseBodyAsync(byte[] content, bool isFinal = true, int requestId = 0)
{
- await SendResponseAsync(body).ConfigureAwait(false);
+ await SendResponseAsync(content).ConfigureAwait(false);
}
public async Task<HttpRequestData> HandleCORSPreFlight(HttpRequestData requestData)
switch (frameType)
{
case Http3FrameType.GoAway:
- await ProcessGoAwayFameAsync(payloadLength).ConfigureAwait(false);
+ await ProcessGoAwayFrameAsync(payloadLength).ConfigureAwait(false);
break;
case Http3FrameType.Settings:
// If an endpoint receives a second SETTINGS frame on the control stream, the endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED.
}
}
- async ValueTask ProcessGoAwayFameAsync(long goawayPayloadLength)
+ async ValueTask ProcessGoAwayFrameAsync(long goawayPayloadLength)
{
long lastStreamId;
int bytesRead;
- while (!VariableLengthIntegerHelper.TryRead(buffer.AvailableSpan, out lastStreamId, out bytesRead))
+ while (!VariableLengthIntegerHelper.TryRead(buffer.ActiveSpan, out lastStreamId, out bytesRead))
{
buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength);
bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false);
// Keep track of how much is remaining in that frame.
private long _requestContentLengthRemaining;
+ // For the precomputed length case, we need to add the DATA framing for the first write only.
+ private bool _singleDataFrameWritten;
+
public long StreamId
{
get => Volatile.Read(ref _streamId);
}
_requestContentLengthRemaining -= buffer.Length;
- if (_sendBuffer.ActiveLength != 0)
+ if (!_singleDataFrameWritten)
{
- // We haven't sent out headers yet, so write them together with the user's content buffer.
+ // Note we may not have sent headers yet; if so, _sendBuffer.ActiveLength will be > 0, and we will write them in a single write.
// Because we have a Content-Length, we can write it in a single DATA frame.
BufferFrameEnvelope(Http3FrameType.Data, remaining);
await _stream.WriteAsync(_gatheredSendBuffer, cancellationToken).ConfigureAwait(false);
_sendBuffer.Discard(_sendBuffer.ActiveLength);
+
+ _singleDataFrameWritten = true;
}
else
{
- // Headers already sent, send just the content buffer directly.
+ // DATA frame already sent, send just the content buffer directly.
await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
}
}
HttpRequestData requestData = await connection.ReadRequestDataAsync();
string requestContent = requestData.Body is null ? (string)null : Encoding.ASCII.GetString(requestData.Body);
Assert.Equal(clientContent, requestContent);
- await connection.SendResponseAsync(HttpStatusCode.OK, body: serverContent);
+ await connection.SendResponseAsync(HttpStatusCode.OK, content: serverContent);
}, new Http2Options() { UseSsl = false });
}
[OuterLoop]
[ConditionalTheory(nameof(IsMsQuicSupported))]
[MemberData(nameof(InteropUris))]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/54726")]
public async Task Public_Interop_ExactVersion_Success(string uri)
{
+ if (UseQuicImplementationProvider == QuicImplementationProviders.Mock)
+ {
+ return;
+ }
+
using HttpClient client = CreateHttpClient();
using HttpRequestMessage request = new HttpRequestMessage
{
[OuterLoop]
[ConditionalTheory(nameof(IsMsQuicSupported))]
[MemberData(nameof(InteropUris))]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/54726")]
public async Task Public_Interop_Upgrade_Success(string uri)
{
+ if (UseQuicImplementationProvider == QuicImplementationProviders.Mock)
+ {
+ return;
+ }
+
using HttpClient client = CreateHttpClient();
// First request uses HTTP/1 or HTTP/2 and receives an Alt-Svc either by header or (with HTTP/2) by frame.
[Fact]
public async Task ConnectTimeout_ConnectCallbackTimesOut_Throws()
{
+ if (UseVersion == HttpVersion.Version30)
+ {
+ // HTTP3 does not support ConnectCallback
+ return;
+ }
+
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
{
using (var handler = CreateHttpClientHandler())
protected override QuicImplementationProvider UseQuicImplementationProvider => QuicImplementationProviders.Mock;
}
- // TODO: Many Cookie tests are failing for HTTP3.
- [ActiveIssue("https://github.com/dotnet/runtime/issues/53093")]
[ConditionalClass(typeof(HttpClientHandlerTestBase), nameof(IsMsQuicSupported))]
public sealed class SocketsHttpHandlerTest_Cookies_Http3_MsQuic : HttpClientHandlerTest_Cookies
{
protected override QuicImplementationProvider UseQuicImplementationProvider => QuicImplementationProviders.MsQuic;
}
- [ActiveIssue("https://github.com/dotnet/runtime/issues/53093")]
[ConditionalClass(typeof(HttpClientHandlerTestBase), nameof(IsMockQuicSupported))]
public sealed class SocketsHttpHandlerTest_Cookies_Http3_Mock : HttpClientHandlerTest_Cookies
{
protected override QuicImplementationProvider UseQuicImplementationProvider => QuicImplementationProviders.Mock;
}
- // TODO: Many cancellation tests are failing for HTTP3.
- [ActiveIssue("https://github.com/dotnet/runtime/issues/53093")]
[ConditionalClass(typeof(HttpClientHandlerTestBase), nameof(IsMsQuicSupported))]
public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http3_MsQuic : SocketsHttpHandler_Cancellation_Test
{
protected override QuicImplementationProvider UseQuicImplementationProvider => QuicImplementationProviders.MsQuic;
}
- [ActiveIssue("https://github.com/dotnet/runtime/issues/53093")]
[ConditionalClass(typeof(HttpClientHandlerTestBase), nameof(IsMockQuicSupported))]
public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http3_Mock : SocketsHttpHandler_Cancellation_Test
{