Kestrel Serving Requests

In this post I review the way Kestrel serves HTTP requests. Please note, that the architecture described here may change from release to release of ASP.NET Core. At the time of writing .NET 7 and the corresponding ASP.NET Core is a month away from release.

Accepting connection on HTTP2 and HTTP3

ASP.NET Core's Kestrel opens IConnectionListener<T>s upon startup. For each ASPNETCORE_URLS set, a connection listener is instantiated. A base IConnectionListener implementation is a SocketConnectionListener. Connection listeners implement the decorator pattern. For example, GenericConnectionListener expects another connection listener as a constructor parameter.Connection listeners provide basic functionality to bind/unbind to a given port and to accept new connection requests.

ConnectionDispatcher also initiated during startup. It starts to run an infinite while loop in AcceptConnectionsAsync() method to accept new connections on a given listener. When a client sends a request, a new connection in initialized. In this case a KestrelConnection<T>, that also implements IThreadPoolWorkItem.Execute().

ConnectionDispatcher dispatches the connection object to the thread pool, so the current thread accepting new requests becomes free again to handle new connection requests.

KestrelConnection<T> provides a few connection level features, like connection lifetime notifications or heartbeat, etc. However, in its execute method it invokes a Func<T, Task> delegate. This delegate comes from the ConnectionDispatcher, and it points to a middleware pipeline.

Notice, that so far there was no mentioning of HTTP. This delegate though (in case of HTTP) can point to an HTTP middleware pipeline. For example, HttpMultiplexedConnectionMiddleware<TContext> middleware is used in case of HTTP3 or HttpsConnectionMiddleware, HttpConnectionMiddleware used in case of HTTP2. From this point on, the goal is to handle HTTP connections. HttpConnectionMiddleware has a reference to the application that represents the request delegates of the host application. This middleware also creates an HttpConnection and instructs it to process the incoming HTTP connection. It passes the application as an argument to ProcessRequestsAsync() method. HttpConnection itself does not do much with a connection. It provides a common abstraction for http connections' initialization/abortion/timeout functionality. However, its main job is to delegate the request further to a request processor, which can either be:

  • Http1Connection

  • Http2Connection

  • Http3Connection

At this point, the execution stack of HTTP2 and HTTP3 requests diverts. I will track the way of handling h2 requests in the rest of this article. Note, that the above mentioned application is passed along to the downstream method calls as an argument.

HTTP2

Upon Http2Connection creation, the new object opens a new internal Pipe and initiates a task that copies the data from the input of socket connection to its internal pipe.

However, the requests gets served by the ProcessRequestsAsync() method, which runs a while loop and reads data from the internal pipe. When data is available, it tries to parse it as an HTTP2 frame. HTTP2 defines a set of frames, for example HEADERS, DATA, SETTINGS, PING or GOAWAY, etc.

Http2Connection does not block the connection handling loop in ProcessRequestsAsync(). Instead, when it reads a frame, for example a complete HEADERS frame, it creates a new Http2Stream object, and passes this object to the thread pool for execution. Http2Connection is responsible to handle the frames and the connection.

Http2Stream continues processing the request. Http2Stream derives from HttpProtocol which provides a shared functionality between HTTP1, HTTP2 and HTTP3. At this point, it tries to parse the request, creates a message body. Once that is successful, it delegates the rest of the work to the host application. Finally, the host application runs the middleware pipelines of the framework/users' code, which eventually ends in controllers or request delegates:

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello " + context.Request.Protocol);
});