| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -79,7 +79,7 @@ is built on the one below and they compose in the same app. | |||
| 79 | 79 | ||
| 80 | 80 | | | | | |
| 81 | 81 | | --- | --- | | |
| 82 | - | **Core** | Routing with constraints and runtime-mutable routes, ASP.NET-shaped middleware, a real `IServiceScope` per request, results in both `Results.*` and `IActionResult` spellings, RFC 9457 problem details and an exception-handler chain | | ||
| 82 | + | **Core** | Routing with constraints and runtime-mutable routes, ASP.NET-shaped middleware that can read and rewrite both bodies of an exchange, a real `IServiceScope` per request, results in both `Results.*` and `IActionResult` spellings, RFC 9457 problem details and an exception-handler chain | | ||
| 83 | 83 | | **Formats** | Content negotiation in both directions — responses chosen from `Accept`, request bodies from `Content-Type`. JSON out of the box; XML, MessagePack and protobuf are one line each, and a format of your own is an `IOutputFormatter`/`IInputFormatter` pair. XML and MessagePack need no dependency and no attributes on your DTOs: they read the same `JsonTypeInfo` the JSON path reads, which is what keeps them AOT-clean where `XmlSerializer` cannot be | | |
| 84 | 84 | | **Protocols** | HTTP/1.1, HTTP/2 (own HPACK), HTTP/3 (own QPACK), WebSockets, Server-Sent Events, trailing headers on all three versions. Never guessed — ALPN over TLS, connection preface over cleartext | | |
| 85 | 85 | | **Content** | Static files from disk *or* embedded resources, a published Blazor WebAssembly app, streaming multipart uploads, downloads with byte ranges and conditional GETs, a file browser over a directory, and brotli/gzip/deflate compression | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,12 @@ triggers: | |||
| 15 | 15 | - MapPost | |
| 16 | 16 | - OnRequest | |
| 17 | 17 | - IHttpMiddleware | |
| 18 | + - IResponseBodyControl | ||
| 19 | + - request logging | ||
| 20 | + - traffic recorder | ||
| 21 | + - capture request body | ||
| 22 | + - capture response body | ||
| 23 | + - read body twice | ||
| 18 | 24 | - RequestDelegate | |
| 19 | 25 | - HttpContext | |
| 20 | 26 | - RouteAttribute | |
@@ -494,8 +500,34 @@ app.UseSessions(); | |||
| 494 | 500 | app.UseStaticFiles("./wwwroot"); | |
| 495 | 501 | ``` | |
| 496 | 502 | ||
| 503 | + ### Seeing the bodies (request logging, traffic recording, audit) | ||
| 504 | + | ||
| 505 | + Both directions have a seam; use them rather than trying to read a body twice off the wire. | ||
| 506 | + | ||
| 507 | + ```csharp | ||
| 508 | + // inbound: read once, hand the handler a rewound copy (also drops any BodyReader handed out) | ||
| 509 | + var buffered = new MemoryStream(); | ||
| 510 | + await ctx.Request.Body.CopyToAsync(buffered, ctx.RequestAborted); | ||
| 511 | + buffered.Position = 0; | ||
| 512 | + ctx.Request.Body = buffered; | ||
| 513 | + | ||
| 514 | + // outbound: wrap the control the response is bound to, then bind the wrapper | ||
| 515 | + var tee = new TeeBodyControl(ctx.Response.BodyControl, capture); // : IResponseBodyControl | ||
| 516 | + ctx.Response.Bind(tee); | ||
| 517 | + try { await next(ctx); } | ||
| 518 | + finally { await tee.FlushAsync(); } // see gotcha below | ||
| 519 | + ``` | ||
| 520 | + | ||
| 521 | + A wrapper must forward `StartAsync`/`CompleteAsync` to the control it wrapped, and should build its | ||
| 522 | + `Writer` over its own `Stream` (not over `inner.Writer`) so both write paths meet in one place. This | ||
| 523 | + is the same seam `UseResponseCompression()` inserts itself through. On a device, decide from the | ||
| 524 | + content type whether a body is worth keeping at all and cap what you store. | ||
| 525 | + | ||
| 497 | 526 | **Critical gotchas:** | |
| 498 | 527 | ||
| 528 | + - **Flush what a body-control wrapper buffered before the pipeline unwinds.** The connection | ||
| 529 | + completes its own producer, not whatever the response ended up bound to, so bytes left in a | ||
| 530 | + wrapper's `PipeWriter` never reach the wire. | ||
| 499 | 531 | - The pipeline is composed **once**, at first serve. Registering middleware after the server starts | |
| 500 | 532 | throws; `RestartAsync` does not recompose it. Routes *can* change at any time. | |
| 501 | 533 | - Headers flush on the first body write. To add a header around a handler, use | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -64,13 +64,29 @@ public string? ContentType | |||
| 64 | 64 | public bool IsChunked { get; internal set; } | |
| 65 | 65 | ||
| 66 | 66 | /// <summary> | |
| 67 | - /// The request body as a stream. Read-only and forward-only — suitable for streaming a large | ||
| 68 | - /// upload straight to disk without buffering. Never null; an empty stream when there is no body. | ||
| 67 | + /// The request body as a stream. As it arrives off the connection it is read-only and | ||
| 68 | + /// forward-only — suitable for streaming a large upload straight to disk without buffering. | ||
| 69 | + /// Never null; an empty stream when there is no body. | ||
| 70 | + /// <para> | ||
| 71 | + /// Settable so a middleware can put something else in front of the handler: the usual reason is | ||
| 72 | + /// to read the body once and hand a rewound <see cref="MemoryStream"/> on, which is what makes | ||
| 73 | + /// a body readable twice — by a recorder or a logger, and then by the handler. Assigning also | ||
| 74 | + /// discards any <see cref="BodyReader"/> already handed out, so the two never disagree about | ||
| 75 | + /// where the body starts. | ||
| 76 | + /// </para> | ||
| 69 | 77 | /// </summary> | |
| 70 | 78 | public Stream Body | |
| 71 | 79 | { | |
| 72 | 80 | get => this.body ??= EmptyReadStream.Instance; | |
| 73 | - internal set => this.body = value; | ||
| 81 | + set | ||
| 82 | + { | ||
| 83 | + ArgumentNullException.ThrowIfNull(value); | ||
| 84 | + this.body = value; | ||
| 85 | + | ||
| 86 | + // a reader built over the old stream would keep reading the old stream, and its | ||
| 87 | + // buffered-but-unconsumed bytes would be lost either way | ||
| 88 | + this.bodyReader = null; | ||
| 89 | + } | ||
| 74 | 90 | } | |
| 75 | 91 | ||
| 76 | 92 | /// <summary> | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -210,13 +210,24 @@ public void Redirect(string location, bool permanent = false, bool preserveMetho | |||
| 210 | 210 | this.Headers.Set(HeaderNames.Location, location); | |
| 211 | 211 | } | |
| 212 | 212 | ||
| 213 | - internal void Bind(IResponseBodyControl bodyControl) => this.control = bodyControl; | ||
| 213 | + /// <summary> | ||
| 214 | + /// Puts <paramref name="bodyControl"/> in charge of framing this response. A middleware that | ||
| 215 | + /// wants to see or transform the body wraps <see cref="BodyControl"/> and binds the wrapper — | ||
| 216 | + /// do it before calling the next delegate, and make sure whatever the wrapper buffered is | ||
| 217 | + /// flushed afterwards, because the connection completes its own producer rather than whatever | ||
| 218 | + /// the response ended up bound to. | ||
| 219 | + /// </summary> | ||
| 220 | + public void Bind(IResponseBodyControl bodyControl) | ||
| 221 | + { | ||
| 222 | + ArgumentNullException.ThrowIfNull(bodyControl); | ||
| 223 | + this.control = bodyControl; | ||
| 224 | + } | ||
| 214 | 225 | ||
| 215 | 226 | /// <summary> | |
| 216 | 227 | /// The control currently framing this response, so a middleware can wrap it — which is how | |
| 217 | 228 | /// response compression inserts itself without every writer knowing about it. | |
| 218 | 229 | /// </summary> | |
| 219 | - internal IResponseBodyControl BodyControl => this.control; | ||
| 230 | + public IResponseBodyControl BodyControl => this.control; | ||
| 220 | 231 | ||
| 221 | 232 | internal async ValueTask InvokeOnStartingAsync() | |
| 222 | 233 | { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,8 +6,16 @@ namespace Shiny.Net.HttpServer; | |||
| 6 | 6 | /// The seam between <see cref="HttpResponse"/> and whatever is actually framing bytes onto the | |
| 7 | 7 | /// wire. Implemented per protocol version, which is what lets HTTP/2 slot in later without | |
| 8 | 8 | /// touching the public response surface. | |
| 9 | + /// <para> | ||
| 10 | + /// Public so a middleware can sit in the middle of it: take the control the response is currently | ||
| 11 | + /// bound to, wrap it, and <see cref="HttpResponse.Bind"/> the wrapper. That is how response | ||
| 12 | + /// compression inserts itself without every writer knowing about it, and it is the same seam a | ||
| 13 | + /// recorder or a logger needs to see the bytes of a body it did not write. Wrappers must forward | ||
| 14 | + /// <see cref="StartAsync"/> and <see cref="CompleteAsync"/> to the control they wrapped, and must | ||
| 15 | + /// flush anything they buffered before the connection completes. | ||
| 16 | + /// </para> | ||
| 9 | 17 | /// </summary> | |
| 10 | - interface IResponseBodyControl | ||
| 18 | + public interface IResponseBodyControl | ||
| 11 | 19 | { | |
| 12 | 20 | bool HasStarted { get; } | |
| 13 | 21 | Stream Stream { get; } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments