diff --git a/src/Titanium.Web.Proxy/Exceptions/ServerConnectionException.cs b/src/Titanium.Web.Proxy/Exceptions/RetryableServerConnectionException.cs similarity index 51% rename from src/Titanium.Web.Proxy/Exceptions/ServerConnectionException.cs rename to src/Titanium.Web.Proxy/Exceptions/RetryableServerConnectionException.cs index d885c74e7..ad07df990 100644 --- a/src/Titanium.Web.Proxy/Exceptions/ServerConnectionException.cs +++ b/src/Titanium.Web.Proxy/Exceptions/RetryableServerConnectionException.cs @@ -3,12 +3,12 @@ namespace Titanium.Web.Proxy.Exceptions { /// - /// The server connection was closed upon first read with the new connection from pool. + /// The server connection was closed upon first write with the new connection from pool. /// Should retry the request with a new connection. /// - public class ServerConnectionException : ProxyException + public class RetryableServerConnectionException : ProxyException { - internal ServerConnectionException(string message) : base(message) + internal RetryableServerConnectionException(string message) : base(message) { } @@ -17,9 +17,8 @@ internal ServerConnectionException(string message) : base(message) /// /// /// - internal ServerConnectionException(string message, Exception e) : base(message, e) + internal RetryableServerConnectionException(string message, Exception e) : base(message, e) { } - } } diff --git a/src/Titanium.Web.Proxy/Network/RetryPolicy.cs b/src/Titanium.Web.Proxy/Network/RetryPolicy.cs index a2f724554..19863cba0 100644 --- a/src/Titanium.Web.Proxy/Network/RetryPolicy.cs +++ b/src/Titanium.Web.Proxy/Network/RetryPolicy.cs @@ -35,14 +35,12 @@ internal async Task ExecuteAsync(Func ReadResponseStatus(CancellationToken cancellationToken = default) { - try - { - string httpStatus = await ReadLineAsync(cancellationToken) ?? - throw new ServerConnectionException("Server connection was closed."); - - if (httpStatus == string.Empty) - { - // is this really possible? - httpStatus = await ReadLineAsync(cancellationToken) ?? - throw new ServerConnectionException("Server connection was closed. Response status is empty."); - } + string httpStatus = await ReadLineAsync(cancellationToken) ?? + throw new IOException("Invalid http status code."); - Response.ParseResponseLine(httpStatus, out var version, out int statusCode, out string description); - return new ResponseStatusInfo { Version = version, StatusCode = statusCode, Description = description }; - } - catch (Exception e) when (!(e is ServerConnectionException)) + if (httpStatus == string.Empty) { - throw new ServerConnectionException("Server connection was closed. Exception while reading the response status.", e); + // is this really possible? + httpStatus = await ReadLineAsync(cancellationToken) ?? + throw new IOException("Response status is empty."); } + + Response.ParseResponseLine(httpStatus, out var version, out int statusCode, out string description); + return new ResponseStatusInfo { Version = version, StatusCode = statusCode, Description = description }; + } } } diff --git a/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs b/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs index 3f4e2f75b..9e38996ee 100644 --- a/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs +++ b/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs @@ -932,7 +932,21 @@ public ValueTask WriteLineAsync(string value, CancellationToken cancellationToke internal async Task WriteHeadersAsync(HeaderBuilder headerBuilder, CancellationToken cancellationToken = default) { var buffer = headerBuilder.GetBuffer(); - await WriteAsync(buffer.Array, buffer.Offset, buffer.Count, true, cancellationToken); + + try + { + await WriteAsync(buffer.Array, buffer.Offset, buffer.Count, true, cancellationToken); + } + catch (IOException e) + { + //throw this as ServerConnectionException so that RetryPolicy can retry with a new server connection. + if (this is HttpServerStream) + { + throw new RetryableServerConnectionException("Server connection was closed. Exception while sending request line and headers.", e); + } + + throw; + } } /// @@ -1226,7 +1240,6 @@ private async Task copyBytesToStream(IHttpStreamWriter writer, long count, bool protected async ValueTask WriteAsync(RequestResponseBase requestResponse, HeaderBuilder headerBuilder, CancellationToken cancellationToken = default) { var body = requestResponse.CompressBodyAndUpdateContentLength(); - headerBuilder.WriteHeaders(requestResponse.Headers); await WriteHeadersAsync(headerBuilder, cancellationToken); diff --git a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs index 429e1dadc..5d1c433fb 100644 --- a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs @@ -520,15 +520,17 @@ internal Task GetServerConnection(ProxyServer proxyServer, if (externalProxy != null && externalProxy.ProxyType == ExternalProxyType.Http && (isConnect || isHttps)) { - var authority = $"{remoteHostName}:{remotePort}".GetByteString(); - var connectRequest = new ConnectRequest(authority) + var authority = $"{remoteHostName}:{remotePort}"; + var authorityBytes = authority.GetByteString(); + var connectRequest = new ConnectRequest(authorityBytes) { IsHttps = isHttps, - RequestUriString8 = authority, + RequestUriString8 = authorityBytes, HttpVersion = httpVersion }; connectRequest.Headers.AddHeader(KnownHeaders.Connection, KnownHeaders.ConnectionKeepAlive); + connectRequest.Headers.AddHeader(KnownHeaders.Host, authority); if (!string.IsNullOrEmpty(externalProxy.UserName) && externalProxy.Password != null) { diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index 377e31d6a..a0bca42c0 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -123,7 +123,7 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam /// /// Number of times to retry upon network failures when connection pool is enabled. /// - public int NetworkFailureRetryAttempts { get; set; } = 0; + public int NetworkFailureRetryAttempts { get; set; } = 1; /// /// Is the proxy currently running? @@ -208,9 +208,9 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam /// /// Maximum number of concurrent connections per remote host in cache. /// Only valid when connection pooling is enabled. - /// Default value is 2. + /// Default value is 4. /// - public int MaxCachedConnections { get; set; } = 2; + public int MaxCachedConnections { get; set; } = 4; /// /// Number of seconds to linger when Tcp connection is in TIME_WAIT state. diff --git a/src/Titanium.Web.Proxy/RequestHandler.cs b/src/Titanium.Web.Proxy/RequestHandler.cs index 103f309f9..005c2daa7 100644 --- a/src/Titanium.Web.Proxy/RequestHandler.cs +++ b/src/Titanium.Web.Proxy/RequestHandler.cs @@ -288,8 +288,13 @@ private async Task handleHttpSessionRequest(SessionEventArgs args, noCache, cancellationToken); - // for connection pool, retry fails until cache is exhausted. - return await retryPolicy().ExecuteAsync(async connection => + /// Retry with new connection if the initial stream.WriteAsync call to server fails. + /// i.e if request line and headers failed to get send. + /// Do not retry after reading data from client stream, + /// because subsequent try will not have data to read from client + /// and will hang at clientStream.ReadAsync call. + /// So, throw RetryableServerConnectionException only when we are sure we can retry safely. + return await retryPolicy().ExecuteAsync(async connection => { // set the connection and send request headers args.HttpClient.SetConnection(connection); diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs b/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs index 84d23eadc..5a95aa8fd 100644 --- a/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs +++ b/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs @@ -202,7 +202,6 @@ public async Task Nested_Proxy_Farm_With_Connection_Cache_Should_Not_Hang() { return Task.FromResult(true); }; - proxies2.Add(proxy2); } @@ -212,9 +211,7 @@ public async Task Nested_Proxy_Farm_With_Connection_Cache_Should_Not_Hang() for (int i = 0; i < 10; i++) { var proxy1 = testSuite.GetProxy(); - //proxy1.EnableConnectionPool = false; var proxy2 = proxies2[rnd.Next() % proxies2.Count]; - var explicitEndpoint = proxy1.ProxyEndPoints.OfType().First(); explicitEndpoint.BeforeTunnelConnectRequest += (_, e) => {