Repository navigation
Web failure log: keep the log batch on a split character, answer bad requests with their own status - #4286
Conversation
…requests with their own status Follow-up to #4281's round-1 review (#4276). Fixes the Medium and four Lows the post-merge security review found. - A 256-char route sanitize cut could split a surrogate pair, and File.AppendAllText's strict UTF-8 encoder throws on a lone surrogate and writes zero bytes -- dropping the whole 5-second log batch, not just the bad line. Sanitize never cuts mid-pair (or leaves any other lone surrogate); Flush uses a permissive, BOM-less UTF-8 encoding as a second line of defense. - The backstop now catches BadHttpRequestException before the generic catch: answers its own status code, writes no body beyond what Kestrel would, and logs at Debug (never Error), so a client triggering a 413/400/408 on purpose no longer costs a 500 and an Error line per request. - Corrected the backstop's comment: it covers the routes only, since it is registered after both gates and a gate throw never enters its try. Pinned EnvironmentName to Production in the web host's WebApplicationOptions so an ambient ASPNETCORE_ENVIRONMENT= Development can never add the developer exception page ahead of the Host guard. - exception.Message now reaches the file log sanitized (control characters mapped to '.', no length cap); the formatted message is left alone because DarlingWorker's "Store host profile" line embeds '\n' on purpose. - The /api/read/* dispatcher's own catch (a binding-layer throw, not a tool's own swallowed exception) now answers the same ruled body and status the top-of-pipeline backstop gives, instead of McpHelpers.FormatError's exception text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…review) File.AppendAllText(path, text) with no encoding uses a strict UTF-8 encoder that throws EncoderFallbackException on a lone surrogate and writes zero bytes, dropping every line in that batch. #4286 fixed Darling's service logger (DarlingFileLoggerProvider.cs); this applies the same new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) third argument to the other seven batch loggers: Darling's ViewerLogger, Lite's AppLogger, MethodProfiler and QueryLogger, and their deprecated Dashboard counterparts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
review) AppLogger.Flush gains an internal FlushTo(logDirectory) seam -- the write half of Flush with the directory as a parameter -- so a test can drive a real batch write without AppLogger.Initialize, which repoints the whole process's static logging and starts a 5s timer (the hazard AppLoggerRetentionTests already documents). Mirrors the CleanOldLogs(directory) and DrainBufferedLines seams added for the same reason. New tests pin both loggers against the old strict-encoder shape: a batch with a lone high surrogate plus a normal line must still write the normal line. Both were reverted to the old File.AppendAllText(path, text) call and confirmed to fail (empty file, EncoderFallbackException swallowed by the catch) before the fix was restored. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Parity fix: the other seven batch loggers get the same non-throwing UTF-8 writePushed as two commits on The fix (all seven files)
All seven already had The one added seam
TestsLite: Darling viewer: Dashboard (deprecated): no test project work, build only, as directed. Revert-proof (both)For each of the two tests, only its target
The fix was then restored in both files, rebuilt, and both passed again. Build results (all three, as directed)
Test results
No full-suite run, per the brief. No CHANGELOG entry was made. The buffer directory was One thing to flagThe brief's commit trailers named session Nothing deferredEverything in the brief is done: all seven files, both tests, both revert-proofs, all three builds. The PR |
Round-1 security review at 5158b33Scope: the diff of this PR at 5158b33. That covers Result: the five findings of the #4281 review are fixed as this PR scopes them. I found no High and no Medium. I found 6 Low items. Most of them are small edits next to code that this PR already changes. Answers to the five questions
Low 1: the MCP host did not get the environment pinWhere: What fails: Who: the same conditions as finding 4. Someone must set the variable, and something must throw. Fix: pin it the same way. var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = Environments.Production,
});Low 2: a client that resets an upload still writes an Error lineWhere: the abort filter at What fails: the filter takes only ASP.NET Core's own exception handler middleware counts Who: the same callers as finding 3. Fix: use the framework's filter. Add a TestServer case that calls catch (Exception ex) when ((ex is OperationCanceledException or IOException)
&& context.RequestAborted.IsCancellationRequested)
{
}Low 3: exception text on the wire in routes that #4283 does not listWhere: What fails: each catch answers 500 with Fix: add the eight sites to #4283. We recommend fixing them in this PR instead, because none of them has the author feedback that keeps the 400 of Low 4: every control-character check here is ASCII-onlyWhere: What fails: all three map the characters below a space, and DEL. The C1 controls (U+0080 to U+009F, including NEL, U+0085) pass, and so do U+2028 and U+2029. Kestrel decodes them from a percent-encoded path, for example A reader that splits lines on them then sees a forged entry. For example, Python's Fix: in all three places, test Low 5: exception text still reaches the formatted message around the new sanitizerWhere: What fails: Today I found no request text on either way. Kestrel's body errors quote no input. Minimal API binding errors do quote the bad value, but the environment pin stops them from throwing. The sweep reads use typed parameters, check Fix:
Low 6: the new environment test does not pin the fix, and it changes process-wide stateWhere: What fails:
Fix: replace the test with a source pin, like the dispatcher pin, that checks that both hosts set Notes, not findings
What I checked and found clean
|
…ks, log sink, sweep GETs (#4281 review) Fixes six Low findings and one review note from the #4286 round-1 security review: pin the MCP host's environment to Production like the web host, take IOException the same as OperationCanceledException on a client abort, treat the C1 controls and U+2028/U+2029 as control characters in the three places that checked ASCII only, clean the whole log line at the file sink instead of only the exception message, answer the two sweep GETs through the dispatcher's failure pattern instead of ex.Message with no log line, replace the environment test with a source pin on both hosts, and guard Sanitize against maxLength 0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Round-1 fixes (review at #4286 (comment))Branch 1. Low 1 — MCP host environment pin
2. Low 2 — abort filter takes IOException too
Test: 3. Low 3 — the two sweep GETs
Test: 4. Low 4 — C1 controls and U+2028/U+2029 in all three placesAll three now check
Tests, each with U+0085 and U+2028:
Revert-proof (both files that gate on the review's own literal repro): reverting 5. Low 5 — sink-level clean,
|
…on text DarlingFleetSweepEndpoints.cs and DarlingWebEndpoints.cs both changed the same catches #4286 touched. Kept this PR's side throughout: the two sweep GETs (/api/sweeps/latest, /api/sweeps/{id}) get no local catch, matching every other route #4283 already strips one from, and the #4281 top-of-pipeline backstop (Mcp/DarlingWebHostService.cs) answers a store fault the same ruled way -- DarlingWebFailureLog.Report, then Body and StatusCode, confirmed unchanged in the merged backstop. Dropped the now-unused System.Diagnostics and PerformanceMonitor.Darling.Service.Hosting usings #4286 had added to DarlingFleetSweepEndpoints.cs for its local catch's Stopwatch and DarlingWebFailureLog. The /api/read/* dispatcher catch in DarlingWebEndpoints.cs only conflicted on its comment; both sides already answer through the same Report/Body/StatusCode shape, so kept this PR's wording, which is the more accurate one post-merge (dev's comment called the ServerError arm's own fix "pending", which this same PR resolves elsewhere in the file). Rewrote FleetSweepWebFeedTests' TheSweepReadCatches_AnswerTheRuledBodyAndStatus_NotExMessage (#4286) as TheSweepReads_HaveNoLocalCatch_NotExMessage, pinning the new shape: neither sweep GET has a local catch, and neither puts ex.Message on the wire. Fixed DarlingWebFailureHandlingTests' ReadDispatchCatch_AnswersTheRuledBodyAndStatus_NotFormatError (#4286), which located the catch block's end by searching for the literal "return ToHttpResult(result);" -- this PR's own ToHttpResult already takes four arguments (result, route, logger, elapsedMs) for its own logging, so that exact literal no longer appears. Matched on the call's start instead of its whole signature. WebExceptionTextCensusTests (#4283) was missing [Collection("live-postgres")] even though its live test opens DARLING_TEST_PG directly -- caught by LivePostgresCollectionHygieneTests once the full suite ran clean of the merge conflicts. Added the attribute so it serializes against the rest of the live-postgres collection like every other class touching the shared store. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Follow-up to #4281, which fixed #4276.
#4281 merged before its security review ran. The review found one Medium and four Lows. This PR fixes all five. It also fixes the same log-writing bug in seven other loggers in Darling, Lite and the Dashboard. A round-1 security review of this PR found six Lows and one note. Commit
117c4623fixes all of them.Why
#4281 added failure handling to the web viewer. The review found these gaps in it:
What changes
1. A split character no longer drops a log batch (Medium)
DarlingHttpRefusalLog.Sanitizecuts request text, such as a route or a Host header, to a fixed length. When the cut fell between the two halves of a surrogate pair, it left a lone half at the end. The file log then wrote its batch withFile.AppendAllTextand a strict UTF-8 encoder. That encoder throws on a lone surrogate and writes nothing, so the whole 5-second batch was lost.Two fixes:
Sanitizenever cuts inside a pair, and it maps any other lone surrogate to..DarlingFileLoggerProviderwrites withnew UTF8Encoding(encoderShouldEmitUTF8Identifier: false). That encoder writes U+FFFD instead of throwing, and it still writes no byte-order mark.2. The same encoder fix in seven other loggers (
a47701fd,6fe73149)Seven more loggers wrote their batches with the strict encoder: Darling's
ViewerLogger, Lite'sAppLogger,MethodProfilerandQueryLogger, and the Dashboard'sLogger,MethodProfilerandQueryLogger. Each now passes the same encoder. Lite has no web server, so only this half of item 1 applies there.AppLoggergainsFlushTo(logDirectory), the write half ofFlush. A test can then write a real batch without callingInitialize, which repoints the whole process's logging and starts a timer. The new Lite test runs in theapp-logger-staticscollection, with the other tests that read the shared log buffer.3. A bad request answers with its own status (Low)
The web host's catch-all now catches
BadHttpRequestExceptionfirst. Kestrel throws it for a body that is too large, malformed or too slow (413, 400 or 408). The answer is that status with no body, and the log line is Debug. The service logs Information and above by default, so a client that sends many bad requests cannot fill the log.A client that drops the connection is not a failure either. When the request was aborted, the catch-all now treats an
IOExceptionthe same as anOperationCanceledException, as ASP.NET Core's own exception handler does. Before, a reset during a body read reached the generic arm and wrote an Error line. (Round 1, Low 2.)4. Both web hosts pin the Production environment (Low)
The web host and the MCP host now build with
EnvironmentName = Environments.Production. Without the pin,ASPNETCORE_ENVIRONMENTorDOTNET_ENVIRONMENTset toDevelopmentanywhere on the machine added the developer exception page ahead of the Host guard. A throw that escaped then answered with the exception text and a stack trace. The MCP host's loopback bind needs no token. (The MCP host's pin is round 1, Low 1.)This PR also corrects the catch-all's comment. The comment said the catch-all sees a throw from the Host guard or the bearer check. It is registered after both, so it sees only throws from the routes.
5. The file log cleans every line (Low)
An exception message can repeat request text. A PostgreSQL cast error quotes the bad value, and
KeyNotFoundExceptionquotes the key. A CR or LF in that text started what looked like a second entry in the file log.DarlingFileLoggerProvidernow cleans each whole line before it writes it. About 80 call sites passex.Messageas a template argument (LogError("... {Message}", ex.Message)), so the text sits in the formatted message, not only in the exception object. Cleaning the whole line covers both. (Round 1, Low 5. This replaces the first version, which cleaned only the exception object's message.)....The Debug line from item 3 no longer passes
bad.Messageas a template argument.6. Every control-character check covers the same characters (round 1, Low 4)
Three checks tested only
c < ' ' || c == 0x7F:DarlingHttpRefusalLog.SanitizeDarlingWebOidc.SubjectCarriesControlCharacters, which refuses a sign-in whose subject has a control characterAll three now use
char.IsControl(c), plus U+2028 and U+2029. So they also catch the C1 range, such as U+0085. Kestrel decodes those from a percent-encoded path such as%C2%85, and an identity provider can put them in a subject claim. U+2028 and U+2029 are Unicode line breaks that some log readers split on.Sanitizealso checks that its cut is above 0 before it reads the character before the cut. No caller passes a length of 0 today. (Round 1 note.)7. Three routes no longer answer with exception text (Low, and round 1's Low 3)
/api/read/*dispatcher's own catch answered withMcpHelpers.FormatError(name, ex), which putsex.Messageon the wire. APostgresExceptionmessage can name schema objects or roles, and an Npgsql connection error names the store's host and port./api/sweeps/latestand/api/sweeps/{id}answered withex.Messageand wrote no log line.All three now work like the catch-all.
DarlingWebFailureLog.Reportwrites the log line. The answer isDarlingWebFailureLog.BodywithDarlingWebFailureLog.StatusCode: a fixed message, with 503 for a statement timeout and 500 for anything else. The body keeps the{"error": "..."}shape that the web pages already read.Not changed here, and tracked by #4283:
ToHttpResult'sServerErrorarm,/api/compose/run, the triage page, and the six Custom View and alert-rule write routes.Tests
DarlingHttpRefusalLogTests: a surrogate pair across the cut is never split, and the output passes a strict UTF-8 encoder. A lone surrogate elsewhere maps to.. U+0085 and U+2028 map to..DarlingFileLoggerProviderTests: a batch with a lone surrogate still writes its other line. A CRLF followed by a fake timestamp stays one entry, and only one line starts with a timestamp. U+0085 and U+2028 map to.. The host profile block keeps each row on its own indented line.DarlingWebFailureHandlingTests: aBadHttpRequestExceptionanswers 413 with an empty body and one Debug line, and no Warning or Error. An abort that surfaces asIOExceptionwrites no Error line. A source check findsEnvironmentName = Environments.Productionin both hosts'CreateBuildercalls. A source check findsDarlingWebFailureLogin the read dispatcher's catch, and noFormatError.FleetSweepWebFeedTests: a source check findsDarlingWebFailureLogin both sweep reads' catches, and noex.Message.DarlingWebOidcTests: a subject with U+0085 or U+2028 is refused.ViewerLoggerTestsand Lite'sAppLoggerSurrogateFlushTests: a batch with a lone surrogate still writes its other line.These tests failed with the product change reverted and passed with it restored:
ViewerLoggerTestsandAppLoggerSurrogateFlushTestsIOExceptionabort testSanitizeand the file logTest plan
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: 0 warnings, 0 errors.Darling.Testson117c4623, once: 14,007 tests, 0 failed, 738 skipped, 1 not run. The skips are the live classes. This run had no PostgreSQL rig.117c4623: build, Darling PostgreSQL tests and Lite tests all pass.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[Web failure log: keep the log batch on a split character, answer bad requests with their own status #4286]: Web failure log: keep the log batch on a split character, answer bad requests with their own status #4286
🤖 Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ