Code/Core/NetworkStorageResponse.cs
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace Sandbox;
public static partial class NetworkStorage
{
private static readonly Dictionary<string, EndpointErrorInfo> _lastEndpointErrors = new();
public static bool TryGetLastEndpointError( string slug, out string code, out string message )
{
if ( _lastEndpointErrors.TryGetValue( slug, out var error ) )
{
code = error.Code;
message = error.Message;
return true;
}
code = null;
message = null;
return false;
}
/// <summary>
/// Parse a server response. Returns the response data on success, null on any error.
/// Detects errors via: ok=false, error object, or non-JSON responses.
/// </summary>
private static JsonElement? ParseResponse( string slug, string raw )
{
if ( string.IsNullOrEmpty( raw ) )
{
if ( NetworkStorageLogConfig.LogErrors )
NetLog.Error( slug, "Server returned empty response" );
RecordEndpointError( slug, "EMPTY_RESPONSE", "Server returned empty response" );
return null;
}
// Catch HTML error pages or non-JSON responses early
var trimmed = raw.TrimStart();
if ( trimmed.Length > 0 && trimmed[0] != '{' && trimmed[0] != '[' )
{
if ( NetworkStorageLogConfig.LogErrors )
NetLog.Error( slug, $"Non-JSON response: {raw[..Math.Min( raw.Length, 120 )]}" );
RecordEndpointError( slug, "INVALID_RESPONSE", "Server returned non-JSON response" );
return null;
}
JsonElement json;
try
{
json = JsonSerializer.Deserialize<JsonElement>( raw );
}
catch
{
if ( NetworkStorageLogConfig.LogErrors )
NetLog.Error( slug, $"Invalid JSON: {raw[..Math.Min( raw.Length, 200 )]}" );
RecordEndpointError( slug, "INVALID_JSON", "Server returned invalid JSON" );
return null;
}
// ── Error detection (multiple patterns for robustness) ──
// Update revision status before error handling too. Revision-expired
// responses intentionally return ok:false but still carry _revisionStatus.
NetworkStoragePackageInfo.UpdateFromServerResponse( json );
var firedRevisionStatusOutdated = false;
if ( json.TryGetProperty( "_revisionStatus", out var statusProp ) && statusProp.ValueKind == JsonValueKind.Object )
{
var statusData = RevisionOutdatedData.FromRevisionStatusJson( statusProp );
if ( statusData.HasValue && statusData.Value.RevisionOutdated )
{
firedRevisionStatusOutdated = true;
OnRevisionOutdated?.Invoke( statusData.Value );
}
}
// 1) Explicit ok: false
if ( json.TryGetProperty( "ok", out var ok ) && ok.ValueKind == JsonValueKind.False )
{
LogServerError( slug, json );
return null;
}
// 2) Error object without ok field (legacy / edge cases)
if ( json.TryGetProperty( "error", out var errProp ) )
{
// { error: { code, message } } — structured error
if ( errProp.ValueKind == JsonValueKind.Object )
{
// Only treat as error if there's no ok:true (some responses include error metadata alongside success)
if ( !json.TryGetProperty( "ok", out var okCheck ) || okCheck.ValueKind != JsonValueKind.True )
{
LogServerError( slug, json );
return null;
}
}
// { error: "string message" } — simple error
else if ( errProp.ValueKind == JsonValueKind.String )
{
if ( NetworkStorageLogConfig.LogErrors )
{
Log.Warning( $"[NetworkStorage] {slug}: {errProp.GetString()}" );
NetLog.Error( slug, errProp.GetString() );
}
RecordEndpointError( slug, errProp.GetString() ?? "UNKNOWN", "" );
return null;
}
}
// 3) HTTP error status forwarded as { status: 4xx/5xx }
if ( json.TryGetProperty( "status", out var status ) && status.ValueKind == JsonValueKind.Number )
{
var statusCode = status.GetInt32();
if ( statusCode >= 400 )
{
LogServerError( slug, json );
return null;
}
}
// Check for load-profile revision block and fire outdated event
if ( !firedRevisionStatusOutdated && json.TryGetProperty( "revision", out var revisionProp ) && revisionProp.ValueKind == JsonValueKind.Object )
{
var data = RevisionOutdatedData.FromJson( revisionProp );
if ( data.HasValue && data.Value.RevisionOutdated )
NetworkStorage.FireRevisionOutdated( data.Value );
}
// Server wraps endpoint responses in { ok, body, timing } — unwrap body if present
if ( json.TryGetProperty( "body", out var body ) && body.ValueKind == JsonValueKind.Object )
return body;
return json;
}
/// <summary>
/// Extract and log error details from a server error response.
/// Handles: { error: { code, message } }, { error: "msg" }, { message: "msg" }
/// </summary>
private static void ClearLastEndpointError( string slug )
{
if ( !string.IsNullOrEmpty( slug ) )
_lastEndpointErrors.Remove( slug );
}
private static void RecordEndpointError( string slug, string code, string message )
{
if ( string.IsNullOrEmpty( slug ) )
return;
var safeCode = string.IsNullOrWhiteSpace( code ) ? "UNKNOWN" : code;
var safeMessage = message ?? "";
_lastEndpointErrors[slug] = new EndpointErrorInfo
{
Code = safeCode,
Message = safeMessage
};
NetworkStorageAnalyticsRuntime.RecordEndpointDiagnostic( slug, ok: false, code: safeCode, message: safeMessage );
}
private sealed class EndpointErrorInfo
{
public string Code { get; init; }
public string Message { get; init; }
}
private static string TruncateJson( JsonElement el ) => TruncateJson( el.ToString() ?? "", 120 );
private static string TruncateJson( string s, int max = 120 )
=> s.Length > max ? s[..max] + "..." : s;
}