FWIW I've already referenced this post: HTTPClient every time returns the same string
...but the circumstances are not parallel:
In my case, after a variable period of time, literally every single response from HttpClient
becomes the exact same identical response string (from the last query it decided to actually process).
It's not always the same request, it can be any one, after maybe 10-20 seconds / 12+ queries. It's just whatever the last one is that it decides to process, all subsequent calls ONLY return whatever the response was to that one.
It doesn't matter if I pass it a brand-new url, if I modify the url with different parameters, or even pass it an entirely different url altogether. It doesn't even matter if I destroy the instance and re-instantiate it completely from scratch! Once it stops actually-processing new queries, the ONLY way to get ANYTHING new out of it is to forcibly terminate the running task, and re-run the application from scratch. Nevertheless, the behaviour always re-asserts itself after maybe a dozen or so queries are submitted.
In attempting to resolve this, I've tried using a statically-defined singleton-instance of HttpClient
in a static class, and I've tried using unique, individually-defined instances for each and every call wrapped up in USING
statements. I've set the 'no-cache' header in PHP on the server/code that it's dereferencing, I've set a content-expiry header (to a date that's well in the past) and I've specifically instantiated the HttpClient object itself with caching turned off by default.
None of these things has had any effect whatsoever in alleviating the issue.
I have been fighting with this headache for MONTHS and have literally no idea what's wrong with this wholly and entirely brain-damaged .NET object/class, and am so-far beyond frustrated with it!
Additional Info:
The app starts by launching an initial Form
that loads configurations, and sets up communications. It tests connectivity and once it has everything working satisfactorily, it accepts username and password credentials and will submit them to the server for authentication. All of the setup/configuration/testing described here is performed in a BackgroundWorker
thread managed by the initial form (so that the form itself continues updating and remains 'live' as it's all happening). The Communications
class is then instantiated with the proper settings and stashed in a public static class accessor that's then used throughout the rest of the application in order to process all server communications.
Once the logon is authenticated, a new 'main menu' Form is instantiated and the log-on Form 'hides' itself. This is usually around the time that the problem occurs, and the ability to do anything more with the application ceases as all new inquiries to the webserver just return the same response (usually from the logon-request) which either crashes the app, or prevents logout/closure. While this is the predominant action, it's not universal: sometimes a few more inquiries will be properly processed. Nevertheless, whichever one is the last one, whatever response it generates becomes the ONLY reply any/all subsequent queries receive, regardless of what URL is submitted.
Both the logon form and the main menu form have active Timer
objects that submit Ping()
calls to the Communications
class at defined intervals in order to keep the connection/session from timing-out as well as ascertaining the latency/status of the connection while the relevant form is open/active.
This is the current implementation of the class I use for communicating with the webserver:
public sealed partial class WebConnection
{
#region Properties
private string _server;
private bool _useSSL = false;
private byte[] _privateKey = Array.Empty<byte>();
private byte[] _sessionId = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private ushort _portId = 0;
private readonly WebConnectionLog _log = new();
#endregion
#region Constructor
public WebConnection( string server, bool useSSL = true, ushort port = 0, string userName = "", string password = "" )
{
this._server = server;
this._portId = port == 0 ? (ushort)(useSSL ? 443 : 80) : port;
this.Password = password;
this.UserName = userName;
this._useSSL = useSSL;
}
#endregion
#region RegexGenerators
[GeneratedRegex( @"(^[a-z][\w]*:\\/\\/|:[\d]+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant )]
private static partial Regex ServerRegex();
[GeneratedRegex( @"[^0-9a-f]", RegexOptions.CultureInvariant )]
private static partial Regex SessionIdRegex();
[GeneratedRegex( @"(\\/\\*|\\*\\/|Master Key|[0-9,]+ bits|[^a-zA-Z0-9+\\/=])", RegexOptions.CultureInvariant )]
private static partial Regex PrivateKeyRegex();
[GeneratedRegex( @"^[0-9a-fA-F]{18}$" )]
private static partial Regex ValidateRegex();
[GeneratedRegex( @"[0-9a-fA-F]+" )]
private static partial Regex HexDecRegex();
[GeneratedRegex( @"[\\/\\\\]$", RegexOptions.CultureInvariant | RegexOptions.Multiline )]
private static partial Regex BuildGetRequestRegex();
[GeneratedRegex( @"[^0-9a-f]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase )]
private static partial Regex HexadecimalCleaner();
[GeneratedRegex( "^[\\s]*[\\d]+[\\d]*$", RegexOptions.CultureInvariant )]
private static partial Regex TimeoutValidator();
[GeneratedRegex( @"^(?<protocol>[a-z]+:[\/\\]{2})?(?<server>.*?[^\\\/.])(?<request>[?&\\\/].+)?$",RegexOptions.IgnoreCase | RegexOptions.CultureInvariant )]
private static partial Regex ValidateServer();
[GeneratedRegex( @"(?<=\[)(?:[0-9a-f]){18}(?=\]$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant )]
private static partial Regex LogoffValidation();
#endregion
#region Accessors
public string Server
{
get => this._server;
set
{
if ( !string.IsNullOrWhiteSpace( value ) )
{
value = ServerRegex().Replace( value, "" );
try
{
Uri test = new( $"http://{value}" ); // Used to validate the form of the string. Will toss an exception if the passed string is garbage...
this._server = test.ToString().Replace( "http://" , "" );
}
catch (UriFormatException) { return; }
catch (Exception) { throw; }
}
}
}
public WebConnectionLog Log => this._log;
public bool UseSSL
{
get => this._useSSL;
set => this._useSSL = value || this._useSSL;
}
public string UserName { get; set; }
public string Password { set; private get; }
public ushort PortId { get; set; } = 443;
public string SessionId
{
get
{
string result = "";
foreach ( byte b in this._sessionId ) result += b.ToString( "x2" );
return result;
}
set
{
if ( !string.IsNullOrWhiteSpace( value ) )
{
List<byte> items = new( new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } );
value = SessionIdRegex().Replace( value.ToLowerInvariant(), "" );
if ( !string.IsNullOrWhiteSpace( value ) && value.Length == 32 )
{
for ( int i = 0; i < 16; i++ )
items[ i ] = HexDec( value.Substring( i * 2, 2 ) );
}
this._sessionId = items.ToArray();
}
}
}
private byte[] SENDER_PRIVATE_KEY => this._privateKey;
public string PrivateKey
{
private get => this._privateKey.ToBase64String();
set
{
if ( (_privateKey is null || _privateKey.Length == 0) && !string.IsNullOrWhiteSpace( value ) )
{
value = PrivateKeyRegex().Replace( value, "" );
this._privateKey = value.FromBase64String();
}
}
}
public string BaseUrl =>
$"http{(this.UseSSL ? "s" : "")}://{Server}{(BuildGetRequestRegex().IsMatch( Server ) ? "" : "/")}";
public static int Timeout { get; set; } = 10000;
/*
{
get => Http.Timeout;
set => Http.Timeout = value;
}
*/
#endregion
#region Methods
private string HttpGetString( Uri url )
{
using var client = new HttpClient() { Timeout = TimeSpan.FromMilliseconds( Timeout ) };
client.DefaultRequestHeaders.CacheControl = new() { NoCache = true };
client.BaseAddress = new Uri( BaseUrl );
var responseTask = client.GetStringAsync( url );
responseTask.Wait();
return responseTask.IsCompletedSuccessfully ? responseTask.Result : responseTask.Exception.Message;
}
private byte[] HttpGetBytes( Uri url )
{
using var client = new HttpClient() { Timeout = TimeSpan.FromMilliseconds( Timeout ) };
client.DefaultRequestHeaders.CacheControl = new() { NoCache = true };
client.BaseAddress = new Uri( BaseUrl );
var responseTask = client.GetByteArrayAsync( url );
responseTask.Wait();
return responseTask.IsCompletedSuccessfully ? responseTask.Result : Array.Empty<byte>();
}
private HttpResponseMessage HttpPost( Uri url, HttpContent data )
{
using var client = new HttpClient() { Timeout = TimeSpan.FromMilliseconds( Timeout ) };
var postResult = client.PostAsync( url, data );
postResult.Wait();
return postResult.IsCompletedSuccessfully ? postResult.Result : null;
}
private HttpResponseMessage HttpPost( Uri url, Dictionary<string, byte[]> data )
{
MultipartFormDataContent content = new();
foreach ( var d in data )
{
var httpContent = new ByteArrayContent( d.Value );
httpContent.Headers.Add( "Content-Type", "application/octet-stream" );
httpContent.Headers.ContentLength = d.Value.Length;
httpContent.Headers.ContentType =
new MediaTypeHeaderValue( "application/octet-stream" );
content.Add( httpContent, d.Key );
//content.Headers.ContentLength = d.Value.Length;
}
return HttpPost( url, content );
}
private HttpResponseMessage HttpPost( Uri url, Dictionary<string, string> data )
{
MultipartFormDataContent content = new();
foreach ( var d in data )
{
var httpContent = new StringContent( d.Value );
httpContent.Headers.Add( "Content-Type", "text/plain; charset=utf-8" );
content.Add( httpContent, d.Key );
}
return HttpPost( url, content );
}
public bool Logon( string userName, string password )
{
KeyValuePair<string, string>[] list = new KeyValuePair<string, string>[2] {
new( "user", userName ), new( "pw", password ) };
Uri req = BuildRequestUri( list );
string result = HttpGetString( req ); // Http.Get( req );
this._log.Add( result, req );
if ( string.IsNullOrEmpty(result) || (result.Length < 47) || (result[..47] == "Response status code does not indicate success:") )// 401 (Unauthorized)." )
return false;
GlobalSettings.User = new( ProcessServerResponse( result, req ) );
return true;
}
public bool Logoff()
{
KeyValuePair<string, string>[] list = new KeyValuePair<string, string>[ 3 ] {
new( "fn", "logoff" ), new( "sessionId", this.SessionId ), new( "senderId", CreateSenderId() ) };
Uri req;
string senderId = "", result = "";
do
{
req = BuildRequestUri( list );
result = HttpGetString( req ); // Http.Get( req );
senderId = LogoffValidation().Match( result ).Value; //HexadecimalCleaner().Replace( result, ""); // Removes non-hexadecimal characters
this._log.Add( result, req );
if ( (result.Length > 18) && (result[ ..18 ] == "Session deleted. [") && ValidateSenderId( senderId ) )
{
this._sessionId = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
return true;
}
} while ( MessageBox.Show( $"{req}\r\n\nThe returned value could not be understood:\r\n\n{result}", $"Log off attempt failed.", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error ) == DialogResult.Retry);
return false;
}
public Uri BuildRequestUri( IEnumerable<KeyValuePair<string, string>> fields, string target = "index.php" )
{
string url = BaseUrl + target;
if ( (fields is not null) && fields.Any() )
foreach( var field in fields )
if (!string.IsNullOrWhiteSpace(field.Key))
url += (url.Contains('?') ? "&" : "?") + $"{field.Key}={Uri.EscapeDataString(field.Value)}";
if ( !string.IsNullOrEmpty( this.SessionId.Replace( "0", "" ) ) )
url += (url.Contains( '?' ) ? "&" : "?") + $"key={this.SessionId}&id={this.CreateSenderId()}";
// Creates a random query value for every formulated uri in order to circumvent caching...
url += (url.Contains( '?' ) ? "&" : "?") + $"pin={int.MaxValue.Random():x8}";
url += this.UseSSL
? (this._portId != 443) ? $":{this._portId}" : ""
: (this._portId != 80) ? $":{this._portId}" : "";
return new Uri( url );
}
public Uri BuildRequestUri( string fn, string value = "", string target = "index.php" ) =>
BuildRequestUri( new KeyValuePair<string, string>[] { new( fn, value ) } );
public Uri BuildRequestUri(string target = "index.php") =>
BuildRequestUri( Array.Empty<KeyValuePair<string, string>>(), target );
/// <summary>Tests the defined server for various levels of connectivity and reports its findings.</summary>
/// <remarks>
/// If the server is reachable, and a valid session exists, calling this function will have the effect of issuing a <i>KeepAlive</i>
/// request, thereby extending the active session.
/// </remarks>
/// <returns>If the initial <b>ICMP</b> poll fails, this will return <seealso cref="long.MinValue"/>, otherwise, if the server is
/// reachable, it will be queried via <b>HTTP</b> and if that result passes validity testing, the length of time taken is returned
/// as a <i>positive</i> integer. If the server is reachable, but fails the <b>HTTP</b> inquiry, the length of time the initial
/// <b>PING</b> test took is reported, but is distinguished from complete success by being <i>a <b>negative</b> value.</i>
/// </returns>
public long Ping()
{
Stopwatch t = new();
Ping p = new();
MatchCollection matches = ValidateServer().Matches( Server );
string server = (matches.Count > 0) && matches[ 0 ].Groups["server"].Success ? matches[ 0 ].Groups["server"].Value : null;
if ( server is not null )
{
PingReply pr = p.Send( server );
if ( pr.Status == IPStatus.Success )
{
t.Start();
var maskArray = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
Uri uri = this.SessionId == "0".PadRight( 32, '0' ) ? new( $"{BaseUrl}?fn=ping" ) : BuildRequestUri( "fn", "keepAlive" );
t.Start();
string result = HttpGetString( uri ); // Http.Get( uri );
t.Stop();
if ( result.IsXml() )
{
this._log.Add( result, uri );
ServerResponse xml = result;
maskArray = this._sessionId;
result = xml.SenderId;
}
else
result = HexadecimalCleaner().Replace( result, "" );
t.Stop();
return ValidateSenderId( result, maskArray ) ? t.ElapsedMilliseconds : -pr.RoundtripTime;
}
}
return long.MinValue;
}
public bool ValidateSenderId( string value ) => ValidateSenderId( value, this._sessionId );
/// <param name="value">The SenderId value to validated!</param>
/// <param name="maskArray">An array of 16 bytes to mask the Sender Id with in order to validate it.</param>
/// <returns><b>TRUE</b> if the supplied <i>value</i> is a valid SenderId.</returns>
public bool ValidateSenderId( string value, byte[] maskArray )
{
bool result = !string.IsNullOrEmpty(value) && ValidateRegex().IsMatch( value );
if ( result )
{
int index = HexDec( value[ 0..2 ] ), i = 0;
value = value[ 2.. ];
do
{
byte mask = maskArray[ i ];
result = (SENDER_PRIVATE_KEY[ index + i ] ^ mask) == HexDec( value.Substring( i * 2, 2 ) );
}
while ( result && (++i < 8) );
}
return result;
}
public string CreateSenderId()
{
int index = RandomNumberGenerator.GetInt32( 0, 248 );
string result = index.ToString( "x2" );
for ( int i = 0; i < 8; i++ )
{
int value = SENDER_PRIVATE_KEY[ index + i ] ^ this._sessionId[i];
result += value.ToString( "x2" );
}
return result;
}
public ServerResponse ProcessServerResponse( string serverResponse, Uri? uri = null )
{
ServerResponse response = new( serverResponse, uri );
if ( (response is null) || !serverResponse.IsXml() )
{
this._log.Add( $"The response from the server is not recognizable.", uri );
throw new InvalidMessageException( this._log.Last().Message );
}
if ( response.HasError && response.SenderId is null )
{
this._log.Add( $"The server's response contains {response.Errors.Count} errors:\r\n• {response.Errors}", response.Uri );
throw new InvalidDataException( this._log.Last().Message );
}
if (response.SessionId is not null)
this.SessionId = response.SessionId;
if ( !ValidateSenderId( response.SenderId ) )
{
this._log.Add( $"Security Breach: An invalid 'senderId' was received with the server response! ({response.SenderId})", response.Uri );
throw new InvalidMessageException( this._log.Last().Message );
}
this._log.Add( serverResponse, response.Uri );
return response;
}
public override string ToString() => BuildRequestUri().ToString();
public static byte HexDec( string value ) =>
string.IsNullOrEmpty(value) || !HexDecRegex().IsMatch(value) ? (byte)0 :
byte.Parse( value[0..2], System.Globalization.NumberStyles.HexNumber );
public ServerResponse PerformGet( IEnumerable<KeyValuePair<string, string>> fields )
{
Uri uri = BuildRequestUri( fields );
//return ProcessServerResponse( Http.Get( uri ), uri );
return ProcessServerResponse( HttpGetString( uri ), uri );
}
public ServerResponse PerformGet( string fn, string value )
{
Uri uri = BuildRequestUri( fn, value );
//return ProcessServerResponse( Http.Get( uri ), uri );
return ProcessServerResponse( HttpGetString( uri ), uri );
}
public ServerResponse PerformGet( Uri uri ) =>
ProcessServerResponse( HttpGetString( uri ), uri ); // ProcessServerResponse( Http.Get( uri ), uri );
public ServerResponse PerformPost( Uri uri, string key, byte[] data )
{
string response = Http.Post( uri, new KeyValuePair<string, byte[]>( key, data ) );
return new( response, uri );
}
public ServerResponse PerformPost( Uri uri, KeyValuePair<string,string> data ) =>
ProcessServerResponse( Http.Post( uri, data.Key, data.Value ), uri );
#endregion
}
...let the flaming begin... ♀️