Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HttpRequestException: Destination is too short. (Parameter 'destination') #78516

Closed
1 task done
BrunoBlanes opened this issue Nov 17, 2022 · 15 comments · Fixed by #78862
Closed
1 task done

HttpRequestException: Destination is too short. (Parameter 'destination') #78516

BrunoBlanes opened this issue Nov 17, 2022 · 15 comments · Fixed by #78862
Labels
area-System.Net.Http bug tenet-reliability Reliability/stability related issue (stress, load problems, etc.)
Milestone

Comments

@BrunoBlanes
Copy link
Contributor

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

While using YARP with HTTP3 enabled via an HttpTransformer the request fails with a System.ArgumentException: Destination is too short. (Parameter 'destination').

Note that in this particular request, it was supposed to download the _framework/blazor.webassembly.js and a couple of CSS files were successfully downloaded before:
image

I have only encountered this error while downloading this particular file if HttpRequestMessage.Version is set to 3.0.

Expected Behavior

Expected the request to complete successfully as it does for most cases, not once did it succeed for this script however.

Steps To Reproduce

  • Create a basic YARP project, enable HTTP3 and set as default version for proxy-to-server communication.
public class Http3ProxyRequestTransformer : HttpTransformer
{
    public override async ValueTask TransformRequestAsync(
	HttpContext httpContext,
	HttpRequestMessage proxyRequest,
	string destinationPrefix)
    {
	await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);

	// Configures all requests messages to use HTTP3
	proxyRequest.Version = new Version(3, 0);
	proxyRequest.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
    }
}
  • Create a Blazor application and enable HTTP3 using ConfigureKestrel and a self-signed certificate:
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(httpsOptions =>
    {
	httpsOptions.ServerCertificateSelector = (_, name) =>
	{
	    return name is not null
		? CertificateLoader.LoadFromStoreCert(
		"dev.localhost",
		"My",
		StoreLocation.CurrentUser,
		false)
		: null;
	};
    });

    serverOptions.ListenAnyIP(5001, listenOptions =>
    {
	listenOptions.UseHttps();
	listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});
  • Forward all requests to the Blazor application:
var httpClient = new HttpMessageInvoker(new SocketsHttpHandler
{
    UseProxy = false,
    AllowAutoRedirect = false,
    AutomaticDecompression = DecompressionMethods.None,
    UseCookies = false
});

var requestConfig = new ForwarderRequestConfig
{
    ActivityTimeout = TimeSpan.FromSeconds(100)
};

app.UseEndpoints(endpoints =>
{
    endpoints.Map("/{**catch-all}", async httpContext =>
    {
	ForwarderError error = httpContext.Request.Host.ToString().Split('.') switch
	{
            [...]

	    _ => await forwarder.SendAsync(
		httpContext,
		Intranet,
		httpClient,
		requestConfig,
		new Http3ProxyRequestTransformer())
	};
    });
});

Exceptions (if any)

info: Yarp.ReverseProxy.Forwarder.HttpForwarder[48]
      Request: An error was encountered before receiving a response.
      System.Net.Http.HttpRequestException: An error occurred while sending the request.
       ---> System.ArgumentException: Destination is too short. (Parameter 'destination')
         at System.Net.Http.QPack.QPackDecoder.DecodeInternal(ReadOnlySpan`1 data, IHttpStreamHeadersHandler handler)
         at System.Net.Http.Http3RequestStream.ReadHeadersAsync(Int64 headersLength, CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.ReadResponseAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         --- End of inner exception stack trace ---
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3Connection.SendAsync(HttpRequestMessage request, Int64 queueStartingTimestamp, CancellationToken cancellationToken)
         at System.Net.Http.HttpConnectionPool.TrySendUsingHttp3Async(HttpRequestMessage request, CancellationToken cancellationToken)
         at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
         at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
         at Yarp.ReverseProxy.Forwarder.HttpForwarder.SendAsync(HttpContext context, String destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer)
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/{**catch-all}'
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HMM89CJQ7T8E", Request id "0HMM89CJQ7T8E:00000015": An unhandled exception was thrown by the application.
      System.Net.Http.HttpRequestException: An error occurred while sending the request.
       ---> System.ArgumentException: Destination is too short. (Parameter 'destination')
         at System.Net.Http.QPack.QPackDecoder.DecodeInternal(ReadOnlySpan`1 data, IHttpStreamHeadersHandler handler)
         at System.Net.Http.Http3RequestStream.ReadHeadersAsync(Int64 headersLength, CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.ReadResponseAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         --- End of inner exception stack trace ---
         at Intervip.ReverseProxy.Startup.<>c__DisplayClass10_0.<<Configure>b__1>d.MoveNext() in C:\Users\bruno\source\repos\Intervip\reverse-proxy\Startup.cs:line 90
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 GET https://dev.localhost/_framework/blazor.webassembly.js - - - 500 0 - 70.8280ms

.NET Version

7.0.100

Anything else?

PS C:\Users\bruno> dotnet --info
.NET SDK:
 Version:   7.0.100
 Commit:    e12b7af219

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22000
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\7.0.100\

Host:
  Version:      7.0.0
  Architecture: x64
  Commit:       d099f075e4

.NET SDKs installed:
  6.0.403 [C:\Program Files\dotnet\sdk]
  7.0.100-rc.1.22431.12 [C:\Program Files\dotnet\sdk]
  7.0.100-rc.2.22477.23 [C:\Program Files\dotnet\sdk]
  7.0.100 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.0-rc.1.22427.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.0-rc.2.22476.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.0-rc.1.22426.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.0-rc.2.22472.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.0-rc.1.22427.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.0-rc.2.22472.13 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  arm64 [C:\Program Files\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\arm64\InstallLocation]
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download
@Tratcher Tratcher transferred this issue from dotnet/aspnetcore Nov 17, 2022
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Nov 17, 2022
@ghost
Copy link

ghost commented Nov 17, 2022

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

While using YARP with HTTP3 enabled via an HttpTransformer the request fails with a System.ArgumentException: Destination is too short. (Parameter 'destination').

Note that in this particular request, it was supposed to download the _framework/blazor.webassembly.js and a couple of CSS files were successfully downloaded before:
image

I have only encountered this error while downloading this particular file if HttpRequestMessage.Version is set to 3.0.

Expected Behavior

Expected the request to complete successfully as it does for most cases, not once did it succeed for this script however.

Steps To Reproduce

  • Create a basic YARP project, enable HTTP3 and set as default version for proxy-to-server communication.
public class Http3ProxyRequestTransformer : HttpTransformer
{
    public override async ValueTask TransformRequestAsync(
	HttpContext httpContext,
	HttpRequestMessage proxyRequest,
	string destinationPrefix)
    {
	await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);

	// Configures all requests messages to use HTTP3
	proxyRequest.Version = new Version(3, 0);
	proxyRequest.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
    }
}
  • Create a Blazor application and enable HTTP3 using ConfigureKestrel and a self-signed certificate:
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(httpsOptions =>
    {
	httpsOptions.ServerCertificateSelector = (_, name) =>
	{
	    return name is not null
		? CertificateLoader.LoadFromStoreCert(
		"dev.localhost",
		"My",
		StoreLocation.CurrentUser,
		false)
		: null;
	};
    });

    serverOptions.ListenAnyIP(5001, listenOptions =>
    {
	listenOptions.UseHttps();
	listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});
  • Forward all requests to the Blazor application:
var httpClient = new HttpMessageInvoker(new SocketsHttpHandler
{
    UseProxy = false,
    AllowAutoRedirect = false,
    AutomaticDecompression = DecompressionMethods.None,
    UseCookies = false
});

var requestConfig = new ForwarderRequestConfig
{
    ActivityTimeout = TimeSpan.FromSeconds(100)
};

app.UseEndpoints(endpoints =>
{
    endpoints.Map("/{**catch-all}", async httpContext =>
    {
	ForwarderError error = httpContext.Request.Host.ToString().Split('.') switch
	{
            [...]

	    _ => await forwarder.SendAsync(
		httpContext,
		Intranet,
		httpClient,
		requestConfig,
		new Http3ProxyRequestTransformer())
	};
    });
});

Exceptions (if any)

info: Yarp.ReverseProxy.Forwarder.HttpForwarder[48]
      Request: An error was encountered before receiving a response.
      System.Net.Http.HttpRequestException: An error occurred while sending the request.
       ---> System.ArgumentException: Destination is too short. (Parameter 'destination')
         at System.Net.Http.QPack.QPackDecoder.DecodeInternal(ReadOnlySpan`1 data, IHttpStreamHeadersHandler handler)
         at System.Net.Http.Http3RequestStream.ReadHeadersAsync(Int64 headersLength, CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.ReadResponseAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         --- End of inner exception stack trace ---
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3Connection.SendAsync(HttpRequestMessage request, Int64 queueStartingTimestamp, CancellationToken cancellationToken)
         at System.Net.Http.HttpConnectionPool.TrySendUsingHttp3Async(HttpRequestMessage request, CancellationToken cancellationToken)
         at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
         at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
         at Yarp.ReverseProxy.Forwarder.HttpForwarder.SendAsync(HttpContext context, String destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer)
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/{**catch-all}'
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HMM89CJQ7T8E", Request id "0HMM89CJQ7T8E:00000015": An unhandled exception was thrown by the application.
      System.Net.Http.HttpRequestException: An error occurred while sending the request.
       ---> System.ArgumentException: Destination is too short. (Parameter 'destination')
         at System.Net.Http.QPack.QPackDecoder.DecodeInternal(ReadOnlySpan`1 data, IHttpStreamHeadersHandler handler)
         at System.Net.Http.Http3RequestStream.ReadHeadersAsync(Int64 headersLength, CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.ReadResponseAsync(CancellationToken cancellationToken)
         at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)
         --- End of inner exception stack trace ---
         at Intervip.ReverseProxy.Startup.<>c__DisplayClass10_0.<<Configure>b__1>d.MoveNext() in C:\Users\bruno\source\repos\Intervip\reverse-proxy\Startup.cs:line 90
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 GET https://dev.localhost/_framework/blazor.webassembly.js - - - 500 0 - 70.8280ms

.NET Version

7.0.100

Anything else?

PS C:\Users\bruno> dotnet --info
.NET SDK:
 Version:   7.0.100
 Commit:    e12b7af219

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22000
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\7.0.100\

Host:
  Version:      7.0.0
  Architecture: x64
  Commit:       d099f075e4

.NET SDKs installed:
  6.0.403 [C:\Program Files\dotnet\sdk]
  7.0.100-rc.1.22431.12 [C:\Program Files\dotnet\sdk]
  7.0.100-rc.2.22477.23 [C:\Program Files\dotnet\sdk]
  7.0.100 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.0-rc.1.22427.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.0-rc.2.22476.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.0-rc.1.22426.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.0-rc.2.22472.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.0-rc.1.22427.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.0-rc.2.22472.13 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  arm64 [C:\Program Files\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\arm64\InstallLocation]
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download
Author: BrunoBlanes
Assignees: -
Labels:

area-System.Net.Http

Milestone: -

@MihaZupan MihaZupan added the bug label Nov 20, 2022
@ManickaP
Copy link
Member

Triage: Looks like QPackDecoder error where we attempt to copy to smaller buffer than necessary. We should fix this for 8.0.

It would be nice to get an isolated repro though. I assume this could be reproduced without reverse proxy, blazor app etc.

@ManickaP ManickaP added this to the 8.0.0 milestone Nov 21, 2022
@ManickaP ManickaP removed the untriaged New issue has not been triaged by the area owner label Nov 21, 2022
@BrunoBlanes
Copy link
Contributor Author

BrunoBlanes commented Nov 22, 2022

I tried reproducing it without the blazor app, by serving just the static blazor.webassembly.js file, but it worked just fine.
I don't quite understand if it needs to have the entire flow of requests made for it to break.
Also I couldn't find a way to force Edge to use HTTP3, so I still had to use a Proxy.

@ManickaP
Copy link
Member

Also I couldn't find a way to force Edge to use HTTP3, so I still had to use a Proxy.

Might be because browsers don't like self-signed certs in QUIC (TLS is part of QUIC itself and the browser cannot let you manually accept the seemingly insecure cert as that's done one layer bellow).

What could work, is having HttpClient directly requesting all those files from the server. In other words, instead of browser -> proxy -> server, having HttpClient -> server. The less moving parts in the repro, the easier to root cause and fix.

@wfurt
Copy link
Member

wfurt commented Nov 23, 2022

It very likely depends on actual value and sequence. If the flow or headers are different you may not trigger it.
You may also try to stop through in Debugger and check the DecodeInternal. At least in VS it should fetch correct sources for you automatically.

@BrunoBlanes
Copy link
Contributor Author

BrunoBlanes commented Nov 23, 2022

I was able to reproduce the error without the need of a proxy, with the help of the default Blazor Hosted Template and the following code:

using var httpClient = new HttpClient();
await GetFileAsync("/_framework/blazor.webassembly.js");

async Task GetFileAsync(string relativeUri)
{
    Console.WriteLine("Requesting file...");
    var request = new HttpRequestMessage
    {
	Method = HttpMethod.Get,
	RequestUri = new Uri(new Uri("https://localhost:5001"), relativeUri),
	Version = new Version(3, 0),
	VersionPolicy = HttpVersionPolicy.RequestVersionExact,
    };

    request.Headers.Add("accept", new[]
    {
	"text/html",
	"application/xhtml+xml",
	"application/xml;q=0.9",
	"image/webp",
	"image/apng",
	"*/*;q=0.8",
	"application/signed-exchange;v=b3;q=0.9"
    });
    request.Headers.Add("accept-encoding", new[] { "gzip", "defalte", "br" });
    request.Headers.Add("accept-language", new[] { "en-US", "en;q=0.9", "pt;q=0.8", "es;q=0.7" });
    request.Headers.Add("cache-control", "no-cache");
    request.Headers.Add("pragma", "no-cache");
    request.Headers.Add("sec-ch-ua", new[]
    {
	@"""Not_A Brand"";v=""99""",
	@"""Microsoft Edge"";v=""109""",
	@"""Chromium"";v=""109"""
    });
    request.Headers.Add("sec-ch-ua-mobile", "?0");
    request.Headers.Add("sec-ch-ua-platform", @"""Windows""");
    request.Headers.Add("sec-fetch-dest", "document");
    request.Headers.Add("sec-fetch-mode", "navigate");
    request.Headers.Add("sec-fetch-site", "none");
    request.Headers.Add("sec-fetch-user", "?1");
    request.Headers.Add("upgrade-insecure-requests", "1");
    request.Headers.Add("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.0.0");

    HttpResponseMessage response = (await httpClient.SendAsync(
	request,
	new CancellationTokenSource().Token))
	.EnsureSuccessStatusCode();
    Console.WriteLine($"File {relativeUri.Split("/")[^1]} received with length {(await response.Content.ReadAsStringAsync()).Length}.");
}

It always breaks on the js file. Also, the headers are what did it for me, without them I couldn't reproduce the error (I just basically copied them from my browser).

I should point out that I tried serving the afore mentioned Blazor static file on its own, but wasn't able to reproduce the error, that is why I still went with a Blazor App.

@BrunoBlanes
Copy link
Contributor Author

You may also try to stop through in Debugger and check the DecodeInternal. At least in VS it should fetch correct sources for you automatically.

I did, and from what I could gather it happens with the "accept-encoding" header and only with the "gzip" value, so essentially this is the only line of code needed for it to crash:

    request.Headers.Add("accept-encoding", new[] { "gzip" });

The exception is thrown at System.ReadOnlySpan<T>.CopyTo(Span<T> destination) method once it gets called by QPackDecoder:

// If a header range was set, but the value was not in the data, then copy the range
// to the name buffer. Must copy because the data will be replaced and the range
// will no longer be valid.
if (_headerNameRange != null)
{
    EnsureStringCapacity(ref _headerNameOctets, _stringLength, existingLength: 0);
    _headerName = _headerNameOctets;

    ReadOnlySpan<byte> headerBytes = data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length);
    headerBytes.CopyTo(_headerName);            
    _headerNameLength = headerBytes.Length;            
    _headerNameRange = null;
}

However I am getting a lot of errors on VS telling me that it could not obtain the value of the local variable, so I wasn't able to learn much about the spans themselves.

@BrunoBlanes
Copy link
Contributor Author

After messing around with my breakpoints, and if I am not mistaken, _headerNameOctets is of length 16, while headerBytes gets created with length 18 which would explain the exception being thrown.

@BrunoBlanes
Copy link
Contributor Author

BrunoBlanes commented Nov 24, 2022

I got VS to decompile the source code and here's what I could find:

DecodeInternal is crashing on these last 25 bytes of header data from a sized 64 buffer:

55 11 98 108 97 122 111 114 45 101 110 118 105 114 111 110 109 101 110 116 11 68 101 118 101
7�blazor-environment�Deve

Comprised above is one header name and the first four bytes of its value, which would eventually become
"blazor-environment":"Development"

Under the QPackeDecoder.ParseCompressedHeaders method this is falling into the Literal Header Field Without Name Reference case, where BeginTryDecode does not succeed and so ParseHeaderNameLength is called:

case 2:
    _Huffman = (b & LiteralHeaderFieldWithoutNameReferenceHuffmanMask) != 0;
    prefixInt = b & LiteralHeaderFieldWithoutNameReferencePrefixMask;

    if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldWithoutNameReferencePrefix, out intResult))
    {
	if (intResult == 0)
	{
		throw new QPackDecodingException(SR.Format(SR.net_http_invalid_header_name, ""));
	}
	OnStringLength(intResult, State.HeaderName);
	ParseHeaderName(data, ref currentIndex, handler);
    }
    else
    {
	_state = State.HeaderNameLength;
	ParseHeaderNameLength(data, ref currentIndex, handler);
    }
    break;

Here, at ParseHeaderNameLength, we learn that the header name length is 18 bytes long and define _headerNameRange accordingly.

Then, at ParseHeaderValueLength we learn that the header value length is to be 11 bytes long.

Once at ParseHeaderValue we get the confirmation that only 4 bytes of the value string are currently available. _stringOctets is currently 32 bytes long, so no change is made to accommodate the remaining data that is to come.

Now that we've gone through the entire buffer, we fall into the following condition:

if (_headerNameRange.HasValue)
{
    EnsureStringCapacity(ref _headerNameOctets, _stringLength, 0);
    _headerName = _headerNameOctets;
    ReadOnlySpan<byte> headerBytes = data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length);
    headerBytes.CopyTo(_headerName);
    _headerNameLength = headerBytes.Length;
    _headerNameRange = null;
}

We've last filled _stringLength with the header value length value of 11, hence now _headerNameOctets will be initialized to a 16 bytes long array. _headerNameRange.length was initialized to be the header name length of 18 bytes. Therefore, we are now trying to copy 18 bytes of data into a 16 bytes array.

I haven't had the time to dive deep into HTTP3 header encoding, so I am not able to pin-point the issue here. However, I find it odd that we don't create the header name given that we already know its value, instead we created a "range buffer". Had this range not been initialized (or been set back to null at some point), this bug would not be present.

@BrunoBlanes
Copy link
Contributor Author

I did, and from what I could gather it happens with the "accept-encoding" header and only with the "gzip" value, so essentially this is the only line of code needed for it to crash:

    request.Headers.Add("accept-encoding", new[] { "gzip" });

Now that I've dug a little deeper, I assume that adding this header to the request message is just a coincidence that makes the server add its matching "content-encoding":"gzip" header to the response for the 64 bytes buffer to be chopped in the perfect way for an unpredicted event.

@BrunoBlanes
Copy link
Contributor Author

Sorry for the lengthy explanation, it is much more for my understanding and sake of mind than anything else.

@ManickaP
Copy link
Member

@BrunoBlanes please don't apologize, this is great investigation. You gave us stable repro and even dived into investigating this, that will help us a lot! Thank you!

@MihaZupan MihaZupan added the tenet-reliability Reliability/stability related issue (stress, load problems, etc.) label Nov 24, 2022
@BrunoBlanes
Copy link
Contributor Author

BrunoBlanes commented Nov 25, 2022

Little update, I decided I am in too deep and began trying to fix it myself, as a learning experience since I haven't messed with the .NET source code before. As such, I came up with the following test for this scenario:

using System.Linq;
using System.Net.Http.QPack;
using System.Net.Http.Unit.Tests.HPack;
using System.Text;
using Xunit;

namespace System.Net.Http.Unit.Tests.QPack
{
    public class QPackDecoderTest
    {
        private const int MaxHeaderFieldSize = 8190;

        // 4.5.6 - Literal Field Without Name Reference - (literal-header-field)
        private static readonly byte[] _literalFieldWithoutNameReference = new byte[] { 0x37, 0x0d, 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2d, 0x66, 0x69, 0x65, 0x6c, 0x64 };

        private const string _headerNameString = "literal-header-field";
        private const string _headerValueString = "should-not-break";

        private static readonly byte[] _headerValueBytes = Encoding.ASCII.GetBytes(_headerValueString);

        private static readonly byte[] _headerValue = new byte[] { (byte)_headerValueBytes.Length }
            .Concat(_headerValueBytes)
            .ToArray();

        private readonly QPackDecoder _decoder;
        private readonly TestHttpHeadersHandler _handler = new TestHttpHeadersHandler();

        public QPackDecoderTest()
        {
            _decoder = new QPackDecoder(MaxHeaderFieldSize);
        }

        [Fact]
        public void DecodesLiteralField_WithoutNameReferece()
        {
            // The key take away here is that the header name length should be bigger than 16 bytes
            // and the header value length less than or equal to 16 bytes and they cannot all be
            // read at once, they must be broken into separate buffers
            byte[] encoded = _literalFieldWithoutNameReference
                .Concat(_headerValue)
                .ToArray();

            _decoder.Decode(new byte[] { 0, 0 }, endHeaders: false, handler: _handler);
            _decoder.Decode(encoded[..^7], endHeaders: false, handler: _handler);
            _decoder.Decode(encoded[^7..], endHeaders: true, handler: _handler);

            Assert.Equal(1, _handler.DecodedHeaders.Count);
            Assert.Equal(_headerNameString, _handler.DecodedHeaders.Keys.First());
            Assert.Equal(_headerValueString, _handler.DecodedHeaders.Values.First());
        }
    }
}

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Nov 26, 2022
@kapryeong
Copy link

kapryeong commented Dec 14, 2022

The same error occurs even when PUT is executed several times in the console program.
7.0.101

  HttpClient client = new()
  {
      DefaultRequestVersion = HttpVersion.Version30,
      DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact
  };
//....//
 foreach (AasAndSubmodels item in list)
 {
      await _httpClient.PutAsJsonAsync($"shells/{item.AAS.IdShort}", item.AAS);
 }
Unhandled exception: System.Net.Http.HttpRequestException: An error occurred while sending the request.
 ---> System.ArgumentException: Destination is too short. (Parameter 'destination')
   at System.Net.Http.QPack.QPackDecoder.DecodeInternal(ReadOnlySpan`1 data, IHttpStreamHeadersHandler handler)
   at System.Net.Http.Http3RequestStream.ReadHeadersAsync(Int64 headersLength, CancellationToken cancellationToken)
   at System.Net.Http.Http3RequestStream.ReadResponseAsync(CancellationToken cancellationToken)
   at System.Net.Http.Http3RequestStream.SendAsync(CancellationToken cancellationToken)

@ghost ghost added in-pr There is an active PR which will close this issue when it is merged and removed in-pr There is an active PR which will close this issue when it is merged labels Apr 6, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Apr 19, 2023
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Apr 25, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label May 15, 2023
@karelz karelz modified the milestones: 8.0.0, 6.0.x May 27, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Jun 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Net.Http bug tenet-reliability Reliability/stability related issue (stress, load problems, etc.)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants