Skip to content

Server Configuration

samgerene edited this page Oct 11, 2026 · 13 revisions

Server Configuration

The ArgusPipeHostBackgroundService is a .NET hosted background service that listens on a named pipe, deserializes incoming ARGUS/1.0 requests, routes them through the middleware pipeline, and returns serialized responses.

Registration

Register the pipe host in your ConfigureServices:

Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddArgusModules();
        services.AddArgusPipeHost(o =>
        {
            o.PipeName = "myservice";
            o.MaxConcurrentRequests = 20;
            o.RequestTimeout = TimeSpan.FromSeconds(120);
        });
    })
    .Build()
    .Run();

AddArgusPipeHost automatically calls AddArgusPlainTextProtocol() to register the default body serializer and registry.

Configuration Options

The ArgusPipeHostOptions class controls server behavior:

Property Type Default Description
PipeName string "argus" The name of the Windows named pipe to listen on
RequestReadTimeout TimeSpan 30 seconds Maximum time a client may take to send its complete request after connecting; then the connection is closed. Set to Timeout.InfiniteTimeSpan to disable
ResponseWriteTimeout TimeSpan 30 seconds Maximum time the host may take to send a complete response to the client; then the connection is closed. Set to Timeout.InfiniteTimeSpan to disable
MaxConcurrentConnections int 100 Maximum number of simultaneous client connections, including those still sending their request. Further clients wait until a connection closes
MaxRequestHeaderSize int 32,768 (32 KB) Maximum size of the request line plus all headers. Larger header blocks are rejected with 400 BadRequest
MaxRequestBodySize long 1,048,576 (1 MB) Maximum allowed request body size in bytes. Requests exceeding this limit are rejected with 400 BadRequest. 0 disables the limit
ShutdownDrainTimeout TimeSpan 30 seconds Maximum time to wait for in-flight requests to complete during shutdown. TimeSpan.Zero cancels them immediately
RequestTimeout TimeSpan 60 seconds Maximum time allowed for a single request handler to execute. Set to Timeout.InfiniteTimeSpan to disable
MaxConcurrentRequests int 10 Maximum number of requests processed concurrently. When the limit is reached, new requests receive 503 ServiceUnavailable
Compression ArgusCompressionOptions disabled Response compression for clients that accept it; compressed requests are always decoded. See Compression
PipeSecurity PipeSecurity null (default ACL) The access control list applied to the pipe on Windows. When null, a default ACL is applied -- see Pipe Security. Ignored on non-Windows platforms

Option Validation

Every option checks the value it is given and throws when it is not valid, so a configuration mistake stops the host at startup with an exception that names the option, instead of failing every request later.

Option Accepted values Exception otherwise
PipeName Not null, empty or white space ArgumentNullException, ArgumentException
MaxConcurrentRequests, MaxConcurrentConnections, MaxRequestHeaderSize Greater than zero ArgumentOutOfRangeException
MaxRequestBodySize Zero (no limit) or greater ArgumentOutOfRangeException
RequestReadTimeout, RequestTimeout, ResponseWriteTimeout At least 1 millisecond and at most about 49.7 days, or Timeout.InfiniteTimeSpan ArgumentOutOfRangeException
ShutdownDrainTimeout Zero or greater and at most about 49.7 days, or Timeout.InfiniteTimeSpan ArgumentOutOfRangeException

The exception is thrown by the assignment itself. With AddArgusPipeHost(o => ...) the configuration callback runs when the host creates its hosted services, so the exception surfaces from IHost.StartAsync (or Run). When the options are bound from configuration (services.Configure<ArgusPipeHostOptions>(configuration.GetSection("Argus"))), the configuration binder sets them through reflection: the host still fails to start, but with a TargetInvocationException whose InnerException is the exception that names the option. The .NET timers count whole milliseconds, so a shorter request timeout would expire immediately, like zero. The upper limit for the timeouts is the longest duration the .NET timers support; use Timeout.InfiniteTimeSpan, not TimeSpan.MaxValue, for "no timeout".

Behavior change: before this validation was added, an invalid value was accepted and failed later: a zero or negative limit stopped the hosted service when it started listening, and a zero or negative timeout made every request fail with only a generic "Pipe request handling failed." in the log. A negative MaxRequestBodySize silently meant "no limit"; use 0 for that. A host that was configured with such a value now fails to start.

Concurrency Limiting

The server uses a semaphore to limit concurrent request processing. When MaxConcurrentRequests is reached, incoming requests receive a 503 ServiceUnavailable problem details response (detail "Server concurrency limit reached", available as ArgusPipeHostBackgroundService.ConcurrencyLimitDetail) without entering the routing pipeline. All of the host's built-in error responses (400, 406, 503, 500) are problem details; see Error Handling.

The server exposes two properties for monitoring:

  • CurrentRequestCount -- the number of requests currently being processed
  • RejectedRequestCount -- the total number of requests rejected due to concurrency limits

Request Timeouts

Each request handler is given a CancellationToken linked to the RequestTimeout. If a handler exceeds the timeout, the token is cancelled and the server returns a 503 ServiceUnavailable problem details response (detail "Request processing timed out", available as ArgusPipeHostBackgroundService.RequestTimeoutDetail).

The cancellation token is available in the handler via context.RequestAborted.

app.MapGet("/long-operation", async context =>
{
    await SomeLongOperation(context.RequestAborted);

    context.Response = new ArgusResponse
    {
        StatusCode = ArgusStatusCode.Ok,
        Body = "Done"
    };
});

Pipe Security (Windows)

On Windows the host creates every pipe instance with an explicit access control list, so that clients in other logon sessions can connect. A typical case is a client running in a user's desktop session that talks to a host running as a Windows service under LocalSystem.

When PipeSecurity is null (the default), the host applies this ACL:

Identity Rights Why
The account running the host FullControl Includes CreateNewInstance, which the host needs to create the next pipe instance after each connection
Authenticated Users ReadWrite, Synchronize Lets any signed-in user's client connect

To restrict access further -- for example to a specific group -- supply your own PipeSecurity. Keep FullControl (or at least CreateNewInstance) for the account running the host. Without it, the host can serve only the first connection and then fails to create new pipe instances:

using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;

services.AddArgusPipeHost(options =>
{
    options.PipeName = "myservice";

    var security = new PipeSecurity();

    // The account running the host must be able to create new pipe instances
    using var current = WindowsIdentity.GetCurrent();
    security.AddAccessRule(new PipeAccessRule(current.User, PipeAccessRights.FullControl, AccessControlType.Allow));

    // Only members of this group may connect as clients
    security.AddAccessRule(new PipeAccessRule(
        new SecurityIdentifier("S-1-5-21-...-1234"),
        PipeAccessRights.ReadWrite | PipeAccessRights.Synchronize,
        AccessControlType.Allow));

    options.PipeSecurity = security;
});

On Linux and macOS, named pipes are UNIX domain sockets and PipeSecurity is ignored. Access there is controlled by file-system permissions on the socket.

Content Negotiation

If a request includes an Accept header, the server negotiates the response serializer from it: the header may list several media ranges with quality values, and type/* and */* are supported. The best acceptable registered serializer writes the response and sets its Content-Type. Only when no registered serializer is acceptable does the server return 406 NotAcceptable. A request without Accept accepts any media type and gets the default serializer. See Accept header rules.

Connection Limits and I/O Timeouts

Three options protect the host against clients that connect but do not send a complete request or do not read their response, whether by accident or deliberately:

  • RequestReadTimeout (30 s by default) limits how long a client may take to send its complete request -- request line, headers and body -- after connecting. When it expires, the host logs a warning ("The client did not send a complete request within …; closing the connection.") and closes the connection; the client sees the connection closed (EndOfStreamException).
  • ResponseWriteTimeout (30 s by default) limits how long the host may take to send the complete response -- status line, headers and body. The host's pipe has no buffer, so the host can only send as fast as the client reads. Without this limit, a client that sends a request and never reads the response would hold its connection, a MaxConcurrentRequests slot and a MaxConcurrentConnections slot until the host stops. When the timeout expires, the host logs a warning ("The response was not sent within …; closing the connection.") and closes the connection; a client that reads later sees the response end early (EndOfStreamException). It applies to every response, including the host's built-in 400, 406 and 503 responses. The limit is a budget for the whole response and not an idle timeout. For a streamed response (BodyStream) it also counts the time your stream takes to produce the data, so a response that streams from a slow or long-lived source is cut off even when the client reads promptly. Increase the timeout, or disable it with Timeout.InfiniteTimeSpan, on a host that streams large or long-lived responses.
  • MaxConcurrentConnections (100 by default) limits the number of simultaneous connections, including connections whose request is still being received. At the limit, the host does not read new connections until one closes. Depending on the platform, further clients either wait in ConnectAsync or connect immediately (on Linux and macOS named pipes are Unix domain sockets and the operating system queues the connection; on Windows one client can connect to the pipe instance the host keeps ready) and then wait until the host reads their request. In both cases the client's own timeout applies.

Together with MaxRequestHeaderSize and MaxRequestBodySize, these bound the memory the host spends on incoming requests. MaxConcurrentRequests additionally limits how many received requests are processed at the same time (503 above the limit).

Resilience to Pipe Errors

If creating a pipe instance or waiting for a connection fails -- for example "All pipe instances are busy" because another process uses the same pipe name, or an access-control problem -- the host logs an error ("Accepting a connection on pipe '…' failed (N consecutive failure(s)); retrying in …") and retries with exponential backoff, starting at 1 second and doubling up to 30 seconds. It keeps running and recovers as soon as the condition clears; only stopping the host ends the loop. A persistent failure shows up as repeated errors in the log.

Header Size Limit

The request line and all headers together may not exceed MaxRequestHeaderSize (32 KB by default). The host stops reading as soon as the limit is exceeded and answers 400 BadRequest ("The header block exceeds the maximum allowed size of N bytes."), so a client cannot exhaust the host's memory with an endless header line or an endless number of headers. Chunk-size lines of chunked bodies are limited to 1 KB.

Body Size Limits

Requests with a Content-Length header exceeding MaxRequestBodySize are rejected with 400 BadRequest before the body is read. Chunked bodies are rejected as soon as they exceed the limit, and compressed bodies are rejected when their decompressed size exceeds it.

Unhandled Exceptions

If a handler or middleware throws, the host logs the exception and returns a generic 500 Internal Server Error problem details response -- the connection is never dropped and no exception details are sent. Register ArgusExceptionHandlerMiddleware with services.AddArgusExceptionHandler() to control the response; see Middleware and Error Handling.

Graceful Shutdown

When the host is stopped (e.g., via Ctrl+C or IHostApplicationLifetime.StopApplication()):

  1. The server stops accepting new pipe connections
  2. In-flight requests are given up to ShutdownDrainTimeout to complete
  3. After the timeout, remaining requests are cancelled via context.RequestAborted and get a short grace period (5 seconds) to finish
  4. If a handler ignores cancellation and is still running after that, the host logs a warning ("N request(s) did not finish after cancellation …") and completes shutdown without disposing the resources that request still uses, so it can finish cleanly

Handlers that do long-running work should pass context.RequestAborted to the operations they await, so shutdown does not have to wait for the grace period.

Minimal Configuration Example

Use all defaults (pipe name "argus", 10 concurrent requests, 60s timeout):

services.AddArgusPipeHost();

Custom configuration:

services.AddArgusPipeHost(options =>
{
    options.PipeName = "myservice";
    options.MaxRequestBodySize = 10_485_760; // 10 MB
    options.MaxConcurrentRequests = 50;
    options.RequestTimeout = TimeSpan.FromMinutes(2);
    options.ShutdownDrainTimeout = TimeSpan.FromSeconds(60);
    options.Compression.Enabled = true;
});

Clone this wiki locally