< Summary

Information
Class: NanoRoute.HttpListenerRouter
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/HttpListener/HttpListenerRouter.cs
Line coverage
100%
Covered lines: 28
Uncovered lines: 0
Coverable lines: 28
Total lines: 168
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBlocks covered Blocks not covered
HttpListenerRouter()80
HttpListenerRouter(...)20
GetRequest(...)360

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/HttpListener/HttpListenerRouter.cs

#LineLine coverage
 1/********************************************************************************
 2* HttpListenerRouter.cs                                                         *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Collections.Frozen;
 8using System.Collections.Generic;
 9using System.IO;
 10using System.Net;
 11using System.Net.Http;
 12using System.Net.Http.Headers;
 13using System.Threading;
 14using System.Threading.Tasks;
 15
 16namespace NanoRoute
 17{
 18    using Internals;
 19
 20    /// <summary>
 21    /// Routes <see cref="HttpListenerContext"/> instances through a NanoRoute pipeline.
 22    /// </summary>
 23    /// <remarks>
 24    /// This adapter converts incoming <see cref="HttpListener"/> traffic into the core
 25    /// <see cref="HttpRequestMessage"/>/<see cref="HttpResponseMessage"/> pipeline used by NanoRoute.
 26    /// </remarks>
 27    /// <example>
 28    /// <code>
 29    /// HttpListenerRouter router = HttpListenerRouter
 30    ///     .CreateBuilder()
 31    ///     .AddDefaultValueParsers()
 32    ///     .AddHandler("GET", "/health/", (context, _) =&gt; Results.Ok())
 33    ///     .CreateRouter();
 34    /// </code>
 35    /// </example>
 36    public sealed class HttpListenerRouter : Router<HttpListenerRouter, HttpListenerRouterConfig>
 37    {
 38        #region Private
 39        // https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistenerresponse.headers?view=net-10.0#remarks
 140        private static readonly FrozenSet<string> s_reservedHeaders = new List<string>
 141        {
 142            "Content-Length",
 143            "Transfer-Encoding",
 144            "Keep-Alive",
 145            "Server"
 146        }.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
 47
 148        private HttpListenerRouter(RouterBuilder<HttpListenerRouter, HttpListenerRouterConfig> builder) : base(builder) 
 49
 50        private static async Task HandleResponse(HttpResponseMessage responseMessage, HttpListenerResponse response, Can
 51        {
 52            response.StatusCode = (int) responseMessage.StatusCode;
 53
 54            CopyResponseHeaders(responseMessage.Headers);
 55
 56            if (responseMessage.Content is not null)
 57            {
 58                CopyResponseHeaders(responseMessage.Content.Headers);
 59
 60                using Stream buffer = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
 61
 62                // https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/librar
 63                await buffer.CopyToAsync(response.OutputStream, 81920, cancellation).ConfigureAwait(false);
 64            }
 65
 66            response.Close();
 67
 68            void CopyResponseHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers)
 69            {
 70                foreach (KeyValuePair<string, IEnumerable<string>> header in headers)
 71                    if (!s_reservedHeaders.Contains(header.Key))
 72                        response.Headers.Add(header.Key, string.Join(",", header.Value));
 73            }
 74        }
 75
 76        private static HttpRequestMessage GetRequest(HttpListenerRequest request)
 177        {
 178            HttpRequestMessage requestMessage = new
 179            (
 180                new HttpMethod(request.HttpMethod),
 181                request.Url
 182            );
 83
 184            if (request.HasEntityBody)
 185                requestMessage.Content = new StreamContent(request.InputStream);
 86
 187            foreach (string headerName in request.Headers.AllKeys)
 188            {
 189                HttpHeaders headers = requestMessage.Content is not null && HttpRequestMessage.ContentHeaders.Contains(h
 190                    ? requestMessage.Content.Headers
 191                    : requestMessage.Headers;
 92
 93                // Some header (like Content-Type) has its default value. Without this line we'd just append the value l
 194                headers.Remove(headerName);
 195                headers.TryAddWithoutValidation(headerName, request.Headers.GetValues(headerName));
 196            }
 97
 198            requestMessage.Properties[OriginalRequestName] = request;
 199            requestMessage.Properties[TraceIdName] = request.RequestTraceIdentifier.ToString("N");
 100
 1101            return requestMessage;
 1102        }
 103        #endregion
 104
 105        /// <summary>
 106        /// Routes a single <see cref="HttpListener"/> request and writes the produced response.
 107        /// </summary>
 108        /// <param name="context">The listener context that supplies the request and receives the response.</param>
 109        /// <param name="services">The service provider exposed to handlers through <see cref="RequestContext.Services"/
 110        /// <param name="cancellation">A token that can cancel request processing and response streaming.</param>
 111        /// <returns>A task that completes after the router has finished writing the response.</returns>
 112        /// <exception cref="ArgumentNullException">
 113        /// Thrown when <paramref name="context"/> or <paramref name="services"/> is <see langword="null"/>.
 114        /// </exception>
 115        /// <exception cref="ArgumentException">Thrown when the request uses an unsupported HTTP method.</exception>
 116        /// <exception cref="HttpRequestException">
 117        /// Thrown when no handler matches the request path or a matched handler signals an HTTP failure that is not
 118        /// translated by middleware.
 119        /// </exception>
 120        /// <exception cref="OperationCanceledException">
 121        /// Thrown when the caller cancels <paramref name="cancellation"/>. The listener response is aborted before the
 122        /// exception is rethrown.
 123        /// </exception>
 124        /// <remarks>
 125        /// Request and content headers are copied into the intermediate <see cref="HttpRequestMessage"/>.
 126        /// The original <see cref="HttpListenerRequest"/> is stored in
 127        /// <see cref="Router.OriginalRequestName"/> on the generated request message.
 128        /// Response headers are copied back except for reserved <see cref="HttpListenerResponse"/> headers that
 129        /// must be managed by <see cref="HttpListener"/> itself. Cancellation is not translated into an HTTP error
 130        /// response by this adapter.
 131        /// </remarks>
 132        /// <example>
 133        /// <code>
 134        /// HttpListenerRouter router = HttpListenerRouter
 135        ///     .CreateBuilder()
 136        ///     .AddJsonErrorDetails()
 137        ///     .AddDefaultValueParsers()
 138        ///     .AddHandler("GET", "/hello/{name:str}/", (context, _) =&gt;
 139        ///         Task.FromResult(HttpResponseMessage.Json(new
 140        ///         {
 141        ///             message = $"Hello {context.Parameters["name"]}"
 142        ///         })))
 143        ///     .CreateRouter();
 144        ///
 145        /// await router.Route(listenerContext, services, cancellationToken);
 146        /// </code>
 147        /// </example>
 148        public async Task Route(HttpListenerContext context, IServiceProvider services, CancellationToken cancellation =
 149        {
 150            Ensure.NotNull(context);
 151            Ensure.NotNull(services);
 152
 153            using HttpRequestMessage request = GetRequest(context.Request);
 154
 155            try
 156            {
 157                using HttpResponseMessage response = await Handle(request, services, cancellation).ConfigureAwait(false)
 158
 159                await HandleResponse(response, context.Response, cancellation).ConfigureAwait(false);
 160            }
 161            catch (OperationCanceledException)
 162            {
 163                context.Response.Abort();
 164                throw;
 165            }
 166        }
 167    }
 168}