Repository navigation
Log and answer web-viewer read failures instead of an empty 500 (#4276) - #4281
Conversation
A read that timed out past the viewer role's statement_timeout reached ASP.NET Core's own error handling, which writes into the log providers the web host clears on purpose - so the browser got an empty 500 and the service log had no trace. Adds one backstop exception handler ahead of every /api/* route: a statement timeout (SQLSTATE 57014, or a client-side Npgsql command timeout) answers 503 with a message the viewer already knows how to show; anything else answers 500 with a generic message, never the exception text; a browser that left the page logs and writes nothing. One shared helper, DarlingWebFailureLog, so the /api/read/* dispatcher's own catch gets the same log line it was missing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…s pin The backstop landed ahead of the DNS-rebinding Host-header guard, which HostHeaderGuardTests pins as the first middleware for a real reason (#1648). Moves it to run after that guard and the auth gate instead, still ahead of every route. Also adds DarlingWebFailureLog.IsStatementTimeout to TsqlConventionGuardTests' KnownTruncatedRanges: it is the same expression-bodied classifier shape as the existing IsCommandTimeout entry right above it, stranding only its own SqlState literal. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
context.Request.Path.Value is request-supplied, and Kestrel decodes a percent-encoded CR/LF in a path into real characters, so an unsanitized route could forge a second log line. Report now sanitizes once, the same way DarlingHttpRefusalLog.Sanitize already handles a Host header, with a 256-char cap so a legitimate long API path isn't cut. Adds a CapturingTestLogger unit test proving a CR/LF-carrying route lands as one inert entry instead of a forged line. Reverting the sanitize call makes the new test fail (Assert.DoesNotContain: sub-string found); restoring it passes again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
|
Pushed What changed
TestAdded DarlingWebFailureLog.Report(logger, "/api/ag\r\nForged: line", 5, new InvalidOperationException());It asserts exactly one Error entry logged ( Revert-proof: temporarily reverted Restored the sanitize call from a backup copy, confirmed the diff matched the original fix exactly, rebuilt clean (0 Warning(s), 0 Error(s)). Test totals
Other notes
🤖 Generated with Claude Code |
Round-1 security review (post-merge) at baaa4e0Scope: the backstop in Result: 1 Medium and 4 Low. Nothing in the change weakens the Host-header guard order, the auth gate, or the tokenless loopback bind. Answers to the four questions
1. Medium: a split surrogate pair drops a whole batch of the service logWhere: What fails: The catch at line 167 then drops the lines that Who can do it: the path must match a route, and the request must then throw. At baaa4e0 the only routes with an unconstrained string path parameter are the three In loopback mode any local process can send it without a token. In network mode it needs a seat that can edit, because the read-only seat check refuses PATCH. One such request in each 5-second flush window loses the lines of that window. Repro: a standalone program, not the shipped binary. It copies The request got a 500 (the same request without the backstop got Fix:
2. Low: the
|
…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
…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
…requests with their own status (#4286) * Web failure log: keep the log batch on a split character, answer bad 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 * #4286 parity: permissive UTF-8 in the other seven batch loggers (#4281 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 * #4286 parity: pin the surrogate fix in AppLogger and ViewerLogger (#4281 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 * #4286 round-1 fixes: environment pin, abort filter, Unicode line breaks, 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 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…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
…ng paths (#4283) (#4293) * Web viewer: stop sending exception text to the browser on the remaining paths (#4283) Round-1 review of #4281 found three older paths that still answered a failed web request with the caught exception's own text; this lane's own grep of the web host found two more. All eight now answer a fixed message and log the real text once through DarlingWebFailureLog, reusing #4281's classifier and messages rather than a second copy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * Route mute-rule ServerError through ServerErrorResult (#4283 H1) MuteRuleToolResult wrapped a tool's caught-exception text straight onto the wire under ErrorResult, never through ServerErrorResult's logged, fixed-message answer the read surface already uses. Classify the result first and route ServerError through ServerErrorResult, with a Stopwatch per mute-rule route matching the /api/read/* loop's timing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * Classify the resolver's own registry-read fault as a ServerError (#4283 H2) The resolver's store-fault sentence ("Could not read the servers registry...") fell through ClassifyToolResponse as a bare ClientError, so ToHttpResult, MuteRuleToolResult and the triage page's note/card all answered it as a 400 with ex.Message on the wire, instead of the generic/timeout 500/503 every other caught exception gets. Factor the sentence's prefix into a constant on DarlingServerResolver, byte-identical to the old inline literal, and recognize it in ClassifyToolResponse alongside the existing "Error during " bare sentence. MCP callers see no change: they read the resolver's sentence back unchanged, never through ClassifyToolResponse. The triage note block gets its own matching branch, since it inspects the resolver's error string directly rather than going through ClassifyToolResponse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * Anchor the sentence timeout token to a known prefix (#4283 L1) s_sentenceTimeoutToken matched \b57014\b anywhere in a tool-caught sentence, so a real SQLSTATE elsewhere in the tail (a quoted value, a port number) could false-positive a 500 into a 503. Anchor the match to the start of the sentence, requiring 57014 immediately after one of the three known prefixes: ErrorSentence's "Error during {op}: ", the compose route's "Error running query: ", and H2's resolver registry- read-fault prefix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4283 review round 1 (M1): route a non-actionable compose/run PostgresException through the web backstop RunComposedPanelAsync's PostgresException catch put every SQLSTATE's real MessageText on the wire at 400, including auth failures (28P01), admin shutdowns (57P01) and other STORE faults a Custom Views panel author cannot act on. Ruling: keep the FIRST design from the review handoff (ComposeRunOutcome gets a Fault field; run_custom_view_panel's MCP answer stays byte-for-byte the same either way). IsComposeRunAuthorActionable classifies 57014 / class 22 / class 42 (except 42501) as author-actionable - those keep "Query failed: {MessageText}" verbatim at 400. Everything else rides back on ComposeRunOutcome.Fault, and the web endpoint mapping (factored into a standalone ComposeRunFailureResult, mirroring ToHttpResult/MuteRuleToolResult so it is directly testable without a live Postgres round trip) answers it through the same fixed-body backstop #4276 gives an uncaught exception: 500, one log line, no role/host text on the wire. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4283 review round 1 (L2): widen WebExceptionTextCensusTests' census past a fixed ex/exception name The census only matched (ex|exception).Message(Text)?, so a differently-named caught exception (pgEx.MessageText) or a bare interpolated {e}/{ex} with no property access at all could reach a web response unseen. Widened the pattern to any leading identifier before .Message/.MessageText/ .InnerException/.Detail/.Hint/.Where (LINQ .Where( excluded) or an ex-shaped .ToString()/{ex} interpolation, matched line-by-line instead of a +-60-char window that could spill a neighboring statement's allow-listed snippet onto an unrelated line. Added DarlingServerResolver.cs and DarlingWebFailureLog.cs to the roster (both now carry exception-text code after H1/H2/M1), with the five new allow-list entries the widened pattern surfaces for real: CustomViewResult/CustomAlertRuleResult's own Conflict.Message/Invalid.Message business text, CollectorRuntimeState's own snapshot.Detail, DarlingWebFailureLog's own exception.InnerException type-pattern check, and DarlingServerResolver's registry-read fault sentence (reclassified through ToHttpResult before the web surface, MCP unchanged, the same shape as ComposeRunOutcome.ServerError's existing allow-list entry). Fixed one bug in the review handoff's own proposed regex along the way: its .ToString() alternative was folded into the unrestricted property-name group, so it matched every unrelated .ToString() in the roster (value.ToString(), m.Archetype.ToString(), builder.ToString()) instead of only an exception- shaped receiver - caught by the handoff's own required negative test case for "count.ToString()", which its literal regex did not actually satisfy. Gave .ToString() its own alternation gated on an ex-shaped identifier to match the handoff's stated design intent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4293 round 2 (R2-L1, R2-L2): a FATAL/PANIC store fault is no longer treated as author-actionable, and a panel's own query error is logged once A FATAL 22023/42704 from a startup parameter the server rejects shared IsComposeRunAuthorActionable's SQLSTATE buckets with a plain ERROR, so it answered 400 with the configured setting's name and value instead of routing through the Fault backstop. Pull the compose runner's PostgresException decision into a testable FromPostgresException helper: author-actionable now also requires ERROR severity, so FATAL/PANIC always carries Fault regardless of SQLSTATE class. ComposeRunOutcome gains a trailing AuthorSqlState member and an AuthorQueryError factory so ComposeRunFailureResult can log a single Warning when it answers a panel's own query error at 400 - store drift (42P01/42703 after an unfinished migration) now reaches the service log, not only one author's browser. The MCP run_custom_view_panel path is unaffected: it reads only Error/IsServerError, unchanged on every arm. Adds FromPostgresException coverage for both arms, a source pin on the catch body, and ComposeRunFailureResult logging coverage for AuthorQueryError vs a plain BadRequest. Updates the stale revert-proof doc comment on the neighboring M1 test to describe what fails now. Darling.Tests: 0 warnings, 0 errors; WebExceptionTextCensusTests 33 (1 skip, no DARLING_TEST_PG), DarlingComposeTests 280, DocCommentHygieneTests 77, all green. Confirmed each revert-proof by hand: dropping the InvariantSeverity clause fails the FATAL cases, reverting the catch to the old ternary fails the source pin, and removing the log call fails the logger test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4293 round 2 (R2-L3): an allow-list snippet now vouches only for the match it contains, not the whole line NoWebEndpoint_BuildsAnAnswerFromExMessage_ExceptTheNamedAllowList used line.Contains(snippet) to decide a match was accounted for - true the moment the snippet appeared ANYWHERE on the offending line, even for a second, unrelated exception-text access sharing that line. Add SnippetCovers(line, snippet, column, length), which scans every occurrence of the snippet on the line and accepts only when the match's own span sits inside one of them. Pull the per-file loop out of the [Fact] into ComputeUnaccounted(relativePath, code) so a test can run the same check against one fabricated line instead of the whole roster. Add SnippetCovers unit coverage (covered, uncovered, a second occurrence), a test running ComputeUnaccounted against the real allow-list on the synthetic line "Query failed: {ex.MessageText} {ex.Detail}" (one unaccounted match, ex.Detail), and a revert-proof pinning that the retired line.Contains check finds the snippet anywhere on that line. Darling.Tests: 0 warnings, 0 errors; WebExceptionTextCensusTests 36 (1 skip, no DARLING_TEST_PG), DocCommentHygieneTests 77, both green. Confirmed by hand: reverting ComputeUnaccounted's SnippetCovers call back to line.Contains(snippet) fails SnippetCovers_RealAllowList_OnASyntheticLine_LeavesExDetailUnaccounted (empty collection instead of the one expected ex.Detail entry). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4293 round 2 (R2-L4): widen s_exMessagePattern to the shapes it still misses, and correct the snapshot.Detail allow-list reason Four widenings to the exception-text census pattern: - A ?, ! or closing ) may now sit before the dot on the generic-receiver alternation: ex?.Message, (ex as PostgresException)?.MessageText, ex.GetBaseException().Message. - A second property list (StackTrace, InternalQuery, TableName, SchemaName, ColumnName, ConstraintName, Routine) gated on an exception-shaped receiver only (ex/exception/e/pgEx/fault/Fault, bare or at the end of a dotted path like outcome.Fault) - TableName/ColumnName are common, harmless names on other receivers, so this list cannot reuse the generic-receiver alternation. - The .ToString()/{...} arms' shared name fragment (now pulled into a private const, ExOrFaultName) also accepts Fault/fault, bare or dotted. - A new alternation catches a string literal concatenated onto an exception-shaped name ("Query failed: " + ex), gated by a negative lookahead so it defers to the other alternations when the name is further accessed. Also corrects the snapshot.Detail allow-list's reason: it is the raw ex.Message of a startup failure today (round-1 H4, tracked in #4316, fixed by PR #4326), not never PostgresException.Detail as it previously read; the entry is to be removed once #4326 lands. Adds every new shape to the must-match list, two non-exception-receiver negatives (widget.TableName, row.ColumnName) proving the new property list's gating, and a revert-proof pinning that the round-1 pattern misses every round-2 shape. Checked every roster file by hand for "fault"/"Fault"/the new property names before widening: DarlingServerResolver.cs has bare fault/Fault tuple variables, none followed by a dot, so none newly match; other hits are inside comments, which StripComments removes. Darling.Tests: 0 warnings, 0 errors; WebExceptionTextCensusTests 37 (1 skip, no DARLING_TEST_PG) - NoWebEndpoint_BuildsAnAnswerFromExMessage_ExceptTheNamedAllowList still reports 0 unaccounted on the real roster files under the widened pattern; DocCommentHygieneTests 77. Both green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4293ci: drop the stale 400 theory row for the resolver's registry-read fault The round-1 H2 fix made ClassifyToolResponse answer ServerError for any sentence starting with DarlingServerResolver.RegistryReadFaultPrefix. An older #3776 theory row still expected 400 for that exact sentence, which CI caught as a live regression against the new behavior. Remove the stale InlineData row, update the theory's summary to describe what still reaches the 400 arm, and add a fact pinning the fault's 500. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Closes #4276.
Why
A web-viewer read that ran past the viewer role's 15-second
statement_timeoutcame back as an empty HTTP 500, and nothing went into the service log./api/ag,/api/fleetand other routes had no try/catch of their own. Their exception reached ASP.NET Core's own error handling. That handling writes to the log providers thatDarlingWebHostServiceclears on purpose, to keep request noise out of the service log. The only trace was in PostgreSQL's own log.What changes
DarlingWebFailureLog(Darling/PerformanceMonitor.Darling.Service/Hosting/DarlingWebFailureLog.cs), tells a statement timeout apart from any other failure. A timeout is aPostgresExceptionwith SQLSTATE 57014, or anNpgsqlExceptionthat wraps aTimeoutException.DarlingHttpRefusalLog.Sanitizebefore it logs it, the same way the refusal log cleans a Host header. A percent-encoded line break in a path cannot start a fake log line. (Commit db1c618.){"error": "..."}body, the same shapeDarlingWebEndpoints.ErrorResultalready writes.DarlingWebHostService.ConfigurePipeline, ahead ofDarlingWebEndpoints.MapAll. MapAll is the only place/api/*routes are mapped, so the handler covers all of them.OperationCanceledExceptionwhileRequestAbortedis cancelled./api/read/*dispatcher (DarlingWebEndpoints.cs) now logs through the same helper when its own catch fires. It no longer turns a browser abort into a written body. It rethrows, and the backstop handles the abort.classifyResponseinwwwroot/js/util.jsalready readsbody.errorfor any response outside 2xx, anderrorStripshows it in the panel.apiGetand every panel loader go through it:pages/ag.js,pages/fleet.js, the Custom View loader inpanels.js, and the server-tab composites.Where the handler sits
The issue sketches the handler at the top of the web pipeline. It sits after
UseResponseCompression, which stays first, and after the Host-header guard (the DNS-rebinding guard) and the network-mode auth gate.HostHeaderGuardTestspins that guard as the firstapp.Usemiddleware because of #1648, an exploited hole on the loopback bind. A handler ahead of the guard breaks that pin. It also puts new code ahead of a security gate for no gain, since both gates handle their own exceptions. The issue's real need still holds: the handler sits ahead of MapAll, so it covers every/api/*route.Test plan
Darling/Darling.Tests/DarlingWebFailureHandlingTests.csis new. Its unit tests cover the classifier: both timeout shapes, the status code, the body and the log line.TestServertests run the realConfigurePipeline, the patternDarlingWebResponseCompressionTestsset up in Darling: a live HTTP test for both hosts' gates, including /core #4128:PostgresExceptionreturns 503 with the JSON body and writes exactly one Warning.DarlingWebEndpoints.MapAll.git stash), all four live tests fail. With it restored, they pass.Report_RouteCarriesCrLf_SanitizesSoNoForgedLineReachesTheLogpasses a route that holds a CR/LF. It checks for exactly one log entry, with no line break in it. With the sanitize call removed, the test fails.origin/dev:DarlingWebFailureHandlingTests15/15 andHostHeaderGuardTests46/46.PerformanceMonitor.Darling.ServiceandDarling.Testsbuild with 0 warnings.HostHeaderGuardTests,DarlingWebFailureHandlingTests,DarlingWebEditorAttributionTests,DarlingWebResponseCompressionTests,DarlingWebHostGateLiveTests.Darling.Testssuite, once, on a freshdarlingteston a UTC rig after mergingorigin/dev: 13,975 total, 3 failed.HostHeaderGuardTests.EachHost_InstallsTheHostHeaderGuard_AsItsFirstMiddlewarefailed because of this PR's first placement. The handler moved behind the guard, as described above, and the class passes.TsqlConventionGuardTests.TheMemberScan_ReadsEveryDeclarationWholefailed because of this PR.DarlingWebFailureLog.IsStatementTimeoutis an expression-bodied member, and the member-range walker reads its range short.PgBaselineProvider.IsCommandTimeouthas the same harmless shape and is already on the allow list.DarlingWebFailureLog.cs IsStatementTimeoutis now inKnownTruncatedRanges, as the test's failure message directs. The class passes alone, 14/14.PgTargetAnomalyTests.TheAuroraWaitProfile_OneHotCollectionStaysQuiet_ASustainedShiftFires_AgainstDevPostgresis the known flake CI flake: PgTarget anomaly/blocking worst-tile assertions fail on first attempts unrelated to the change #4274 and touches none of this PR's files. It passed alone on the same rig, 44/44.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[Log and answer web-viewer read failures instead of an empty 500 (#4276) #4281]: Log and answer web-viewer read failures instead of an empty 500 (#4276) #4281
🤖 Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ