Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions src/Fluxzy.Core/Clients/H11/Http11ConnectionPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Fluxzy.Core;
using Fluxzy.Misc.ResizableBuffers;
using Fluxzy.Writers;
using Org.BouncyCastle.Tls;

namespace Fluxzy.Clients.H11
{
Expand Down Expand Up @@ -83,6 +86,7 @@ public async ValueTask Send(
}

exchange.Connection = state.Connection;
exchange.RecycledConnection = true;
break;
}

Expand Down Expand Up @@ -153,14 +157,35 @@ void OnExchangeCompleteFunction(Task<bool> completeTask)
}
catch (Exception ex) {

if (ex is ConnectionCloseException)
{
if (exchange.Connection.ReadStream != null)
// Any "connection is dead" signal must dispose the read stream and
// null the connection so the next attempt opens a fresh one. The
// original code only handled ConnectionCloseException, leaking the
// read stream when TlsFatalAlert / IOException / SocketException
// bubbled through unconverted (e.g. from the request-write path).
var deadConnSignal = ex is ConnectionCloseException
|| ex is TlsFatalAlert
|| ex is IOException
|| ex is SocketException;

if (deadConnSignal) {
if (exchange.Connection?.ReadStream != null)
await exchange.Connection.ReadStream.DisposeAsync();

exchange.Connection = null;
exchange.Connection = null;
}


// Recycled connection that died before producing any response byte —
// safe to relaunch on a fresh connection regardless of whether the
// failure happened on the request write or the response read. The
// recycled-and-no-response gate keeps a fresh-connection failure
// (server closes immediately) flowing through as 528.
if (deadConnSignal
&& !(ex is ConnectionCloseException)
&& exchange.RecycledConnection
&& exchange.Metrics.ResponseHeaderStart == default) {
throw new ConnectionCloseException("Relaunch");
}

throw;
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/Fluxzy.Core/Clients/H11/Http11PoolProcessing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -150,6 +151,14 @@ await ForwardInterimToClient(exchange, 100, cancellationToken)
cancellationToken,
true).ConfigureAwait(false);

// Close-notify path: GetNext signals close_notify by returning
// HeaderLength = -1. Skip the interim-response sniff (which would
// index into the buffer with a negative length) and fall through
// so the post-try CloseNotify branch can throw the relaunch
// exception.
if (headerBlockDetectResult.CloseNotify)
break;

var earlyStatus = HttpHelper.ReadStatusCode(
buffer.Buffer.AsSpan(0, headerBlockDetectResult.HeaderLength));

Expand All @@ -167,7 +176,20 @@ await ForwardInterimToClient(exchange, earlyStatus, cancellationToken)
}
}
catch (Exception ex) {
if (ex is TlsFatalAlert || (exchange.Context.EventNotifierStream?.Faulted ?? false)) {
// A read failure on a connection that came from the pool, before any
// response byte has been seen, means the upstream tore the connection
// down while it was idle (TLS close_notify + FIN, or an outright RST).
// Map to ConnectionCloseException so the orchestrator retries on a
// fresh connection. The recycled-and-no-response gate is what keeps a
// genuine fresh-connection failure (server closes before responding)
// surfacing as 528 instead of looping.

var noResponseByteYet = exchange.Metrics.ResponseHeaderStart == default;
var recycledAndDead = exchange.RecycledConnection && noResponseByteYet;

if (ex is TlsFatalAlert
|| (exchange.Context.EventNotifierStream?.Faulted ?? false)
|| (recycledAndDead && (ex is IOException || ex is SocketException))) {
throw new ConnectionCloseException("Relaunch");
}

Expand Down
11 changes: 11 additions & 0 deletions src/Fluxzy.Core/Core/Exchange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,17 @@ public Exchange(

public bool ReadUntilClose { get; set; }

/// <summary>
/// True when this exchange is being processed on a connection that
/// was reused from the pool (rather than freshly opened). The
/// HTTP/1.1 pool sets this so the response-read failure path can
/// tell "stale pooled connection died on us" (safe to relaunch on
/// a fresh connection) apart from "fresh connection failed
/// immediately" (genuine upstream error — must surface to caller,
/// not retry).
/// </summary>
public bool RecycledConnection { get; set; }

public int StreamIdentifier { get; set; }

/// <summary>
Expand Down
9 changes: 8 additions & 1 deletion src/Fluxzy.Core/Misc/Streams/DisposeEventNotifierStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ public override int Read(byte[] buffer, int offset, int count)

public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = new())
{
return await InnerStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
try {
return await InnerStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
}
catch {
Faulted = true;

throw;
}
}

public override async Task<int> ReadAsync(
Expand Down
Loading
Loading