From ada5baa85066204fe4f7ccc54249468b3cea1a24 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 24 Aug 2026 13:39:30 +0100 Subject: [PATCH 1/2] Move the help link off the end of the timeout message The timeout message ended "...that can cause timeouts: https://seredis.dev/Timeouts)", and link detectors in log viewers, chat and issue trackers take the closing bracket with them. The docs site sees the result: /Timeouts) is far and away its most requested missing page, along with /Timeouts)), /Timeouts)%22 (the message inside a quoted string) and /Timeouts)/r/n (a log line's trailing escape, with URL parsing reading the backslashes as slashes). Putting the URL mid-sentence with a space after it is all it takes - every detector stops at the space. The message is a little shorter for it too. This only helps versions built from here on; everything already shipped keeps emitting the old text, so the site needs a redirect regardless. --- src/StackExchange.Redis/ExceptionFactory.cs | 7 +++++-- tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs | 4 ++-- tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index d47d62c97..aa9bf7001 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -309,9 +309,12 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? bas } AddCommonDetail(data, sb, message, multiplexer, server); - sb.Append(" (Please take a look at this article for some common client-side issues that can cause timeouts: ") + // Keep the URL away from the end of the message and away from punctuation: linkifiers in log + // viewers, chat and issue trackers greedily swallow a trailing ')' or '"', and the docs site + // then sees a 404 for /Timeouts). The space after it is what makes them stop. + sb.Append(" (see ") .Append(TimeoutHelpLink) - .Append(')'); + .Append(" for some common client-side issues that can cause timeouts)"); // If we're from a backlog timeout scenario, we log a more intuitive connection exception for the timeout...because the timeout was a symptom // and we have a more direct cause: we had no connection to send it on. diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index 0c156300c..e2c34c9ad 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -49,7 +49,7 @@ public async Task DisconnectAndReconnectThrowsConnectionExceptionSync() Assert.SkipUnless(server.CanSimulateConnectionFailure, "Skipping because server cannot simulate connection failure"); server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 10 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 13, qu: 1, qs: 0, aw: False, bw: Inactive, last-in: 0, cur-in: 0, sync-ops: 2, async-ops: 0, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=2,Free=32765,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=6237,Timers=39), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://seredis.dev/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 10 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 13, qu: 1, qs: 0, aw: False, bw: Inactive, last-in: 0, cur-in: 0, sync-ops: 2, async-ops: 0, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=2,Free=32765,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=6237,Timers=39), v: 2.6.120.51136 (see https://seredis.dev/Timeouts for some common client-side issues that can cause timeouts) var ex = Assert.ThrowsAny(() => db.Ping()); Log("Exception: " + ex.Message); Assert.True(ex is RedisConnectionException or RedisTimeoutException); @@ -75,7 +75,7 @@ public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() Assert.SkipUnless(server.CanSimulateConnectionFailure, "Skipping because server cannot simulate connection failure"); server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 8 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 8 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=6,Free=32761,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://seredis.dev/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 8 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 8 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=6,Free=32761,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60), v: 2.6.120.51136 (see https://seredis.dev/Timeouts for some common client-side issues that can cause timeouts) var ex = await Assert.ThrowsAsync(() => db.PingAsync()); Log("Exception: " + ex.Message); Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (1000ms) - Last Connection Exception: ", ex.Message); diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index ea868fb46..00d39b60d 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -116,7 +116,7 @@ public async Task TimeoutException() var ex = Assert.IsType(rawEx); Log("Exception: " + ex.Message); - // Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://seredis.dev/Timeouts)"; + // Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (see https://seredis.dev/Timeouts for some common client-side issues that can cause timeouts)"; Assert.StartsWith("Test Timeout, command=PING", ex.Message); Assert.Contains("clientName: " + nameof(TimeoutException), ex.Message); // Ensure our pipe numbers are in place @@ -138,7 +138,7 @@ public async Task TimeoutException() Assert.Contains("Timers=", ex.Message); #endif Assert.DoesNotContain("Unspecified/", ex.Message); - Assert.EndsWith(" (Please take a look at this article for some common client-side issues that can cause timeouts: https://seredis.dev/Timeouts)", ex.Message); + Assert.EndsWith(" (see https://seredis.dev/Timeouts for some common client-side issues that can cause timeouts)", ex.Message); Assert.Null(ex.InnerException); } finally From b3371ee643a3e3ec0bdc229d6c1dd8128c0c81f9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 24 Aug 2026 13:39:40 +0100 Subject: [PATCH 2/2] Docs: link the rendered pages, and give the site a 404 page of its own Five links pointed at the markdown source rather than the page: Transactions.md and KeysValues.md from the CAS page, Profiling_v1.md/Profiling_v2.md, and Timeouts.md from the 2.0 release notes. Those resolve - the site serves the sources alongside the built pages - but as raw text/markdown, with no styling and no way onward. The site was also using GitHub's stock 404 page, which offers nothing. Ours points at the places people are usually looking for, and, since the missing page is so often a URL pasted out of an exception message with the sentence's punctuation stuck to it, offers the address with that punctuation stripped. It suggests rather than redirects, so a bad guess costs nothing; the strip is idempotent, so there is no loop. Also made the bare release-notes URL in the readme an explicit link, for the same reason the timeout message no longer ends in one. --- README.md | 2 +- docs/404.html | 50 ++++++++++++++++++++++++++++++++++++++++++ docs/CompareAndSwap.md | 4 ++-- docs/Profiling.md | 4 ++-- docs/ReleaseNotes.md | 2 +- 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 docs/404.html diff --git a/README.md b/README.md index f8c6c4fbd..1e7535701 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ MyGet Pre-release feed: https://www.myget.org/gallery/stackoverflow | ------- | ------------ | ----------------- | --------- | ----- | | [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/absoluteLatest) | [![StackExchange.Redis](https://img.shields.io/nuget/dt/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -Release notes at: https://github.com/StackExchange/StackExchange.Redis/releases (3.0 onwards; earlier releases at https://seredis.dev/ReleaseNotes) +Release notes at: [GitHub Releases](https://github.com/StackExchange/StackExchange.Redis/releases) (3.0 onwards; [earlier releases](https://seredis.dev/ReleaseNotes)) diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 000000000..cb4dd0ac4 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,50 @@ +--- +layout: default +title: Page not found +sitemap: false +--- + +

There is nothing at this address.

+ + + +

If you got here from a link in an exception message or a log line, the punctuation after the URL was +probably picked up as part of it — most link detectors will happily swallow a closing bracket or a +quote.

+ + + + diff --git a/docs/CompareAndSwap.md b/docs/CompareAndSwap.md index 7d79d42a0..c88347b12 100644 --- a/docs/CompareAndSwap.md +++ b/docs/CompareAndSwap.md @@ -316,6 +316,6 @@ var condition = ValueCondition.CalculateDigest(largeValueBytes); ## See Also -- [Transactions](Transactions.md) - For multi-key atomic operations -- [Keys and Values](KeysValues.md) - Understanding Redis data types +- [Transactions](Transactions) - For multi-key atomic operations +- [Keys and Values](KeysValues) - Understanding Redis data types - [Redis CAS/CAD Documentation](https://redis.io/docs/latest/commands/set/) - Redis 8.4 SET command with IFEQ/IFNE/IFDEQ/IFDNE modifiers diff --git a/docs/Profiling.md b/docs/Profiling.md index cffdae25d..6b34f55e6 100644 --- a/docs/Profiling.md +++ b/docs/Profiling.md @@ -5,6 +5,6 @@ The profiling API has undergone breaking changes between 1.* and 2.*; in 1.*; in the `object GetContext()` API was unintuitive for consumers and expensive for the library (due to book-keeping). The API in 2.* is much simpler and more "obvious". This is a breaking change. -[Profiling in 1.\*](Profiling_v1.md) +[Profiling in 1.\*](Profiling_v1) -[Profiling in 2.\*](Profiling_v2.md) +[Profiling in 2.\*](Profiling_v2) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 14231ff50..500a416dd 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -613,7 +613,7 @@ Changes: - **Soft Break**: The `PreserveAsyncOrder` behaviour of the pub/sub API has been deprecated; a *new* API has been provided for scenarios that require in-order pub/sub handling - the `Subscribe` method has a new overload *without* a handler parameter which returns a `ChannelMessageQueue`, which provides `async` ordered access to messages) - Internal: The network architecture has moved to use `System.IO.Pipelines`; this has allowed us to simplify and unify a lot of the network code, and in particular fix a lot of problems relating to how the library worked with TLS and/or .NETStandard -- Change: As a result of the `System.IO.Pipelines` change, the error-reporting on timeouts is now much simpler and clearer; the [timeouts documentation](Timeouts.md) has been updated +- Change: As a result of the `System.IO.Pipelines` change, the error-reporting on timeouts is now much simpler and clearer; the [timeouts documentation](Timeouts) has been updated - Removed: The `HighPriority` (queue-jumping) flag is now deprecated - Internal: Most buffers internally now make use of pooled memory; `RedisValue` no longer preemptively allocates buffers - Internal: Added new custom thread-pool for handling async continuations to avoid thread-pool starvation issues