Repository navigation
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.
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.
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 |
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
MaxRequestBodySizesilently meant "no limit"; use0for that. A host that was configured with such a value now fails to start.
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
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"
};
});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.
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.
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, aMaxConcurrentRequestsslot and aMaxConcurrentConnectionsslot 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-in400,406and503responses. 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 withTimeout.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 inConnectAsyncor 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).
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.
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.
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.
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.
When the host is stopped (e.g., via Ctrl+C or IHostApplicationLifetime.StopApplication()):
- The server stops accepting new pipe connections
- In-flight requests are given up to
ShutdownDrainTimeoutto complete - After the timeout, remaining requests are cancelled via
context.RequestAbortedand get a short grace period (5 seconds) to finish - 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.
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;
});copyright @ Sam Gerené