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
102 changes: 102 additions & 0 deletions docs/528-error-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 528 transport error and `x-fluxzy-network-error`

When Fluxzy fails to relay a request because of an upstream network, DNS, or TLS
problem it returns a synthesized `528 Fluxzy transport error` response (or `502
Bad Gateway` when `FluxzySharedSetting.Use528 = false`).

To let programmatic consumers react without parsing the body, every synthesized
528 carries an extra response header:

```
x-fluxzy-network-error: <token>
```

The same token is persisted on `ClientError.NetworkErrorCode` in archives
(`.fxzy` / HAR) so post-mortem readers can recover it.

The header value is one of the stable identifiers below, defined as
`public const string` on `Fluxzy.Core.NetworkErrorCodes`. The strings are
considered a public contract and will not change without a major version bump.

## Token reference

### Connection layer

| Token | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------- |
| `connection_refused` | The remote peer responded but actively refused the TCP connection (RST on SYN). |
| `connection_reset` | The remote peer reset an established connection. |
| `connection_aborted` | The connection was aborted, often because the remote half closed without a clean FIN. |
| `connection_timeout` | The remote peer could not be contacted within the configured TCP connect timeout. |
| `host_unreachable` | The OS reports the remote host as unreachable (no route, ICMP host-unreachable). |
| `network_unreachable` | The OS reports the remote network as unreachable (no route at the network level). |
| `connection_closed` | The remote peer closed the TCP connection while Fluxzy was reading the response header. |

### DNS layer

| Token | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `dns_notfound` | The DNS server returned NXDOMAIN: the requested host does not exist. |
| `dns_no_data` | The DNS server resolved the name but returned no usable A/AAAA record. |
| `dns_try_again` | The DNS server returned a transient failure (SERVFAIL or equivalent); a retry might succeed. |
| `dns_failure` | Generic DNS resolution failure: malformed response, unreachable resolver, DoH endpoint returning non-2xx. |

### TLS layer

| Token | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `tls_cert_expired` | The server certificate is past its validity period (or not yet valid). |
| `tls_cert_hostname_mismatch` | The server certificate is valid but does not cover the requested hostname (Subject / SAN mismatch). |
| `tls_cert_untrusted` | The server certificate chain does not chain to a trusted root (untrusted root, partial chain, no policy). |
| `tls_cert_invalid` | Other certificate-policy failure: revoked, malformed, unknown to the validator, or required but missing. |
| `tls_handshake_failure` | TLS handshake failed for a reason that is not specific to the certificate (alert, version, cipher, ...). |

### Other

| Token | Description |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `protocol_error` | An HTTP/2 stream protocol error happened before the response header was received. |
| `rule_failure` | A user-supplied Fluxzy rule (filter or action) threw during exchange processing. See the body for the rule diagnostic. |
| `unknown` | Fallback when none of the categories above match. Typically a wrapped exception that did not carry enough information. |

## Example

```http
HTTP/1.1 528 Fluxzy error
x-fluxzy: Fluxzy error
x-fluxzy-network-error: tls_cert_expired
Content-length: 7520
Content-type: text/html; charset=utf-8
Connection: close

<html>...</html>
```

## Reading the token from C#

```csharp
using var response = await httpClient.GetAsync(url);

if ((int)response.StatusCode == 528 &&
response.Headers.TryGetValues("x-fluxzy-network-error", out var values))
{
var token = values.Single();
// e.g. switch on Fluxzy.Core.NetworkErrorCodes.* constants
}
```

## Reading the token from an archive

```csharp
foreach (var clientError in exchange.ClientErrors)
{
var token = clientError.NetworkErrorCode; // null on success
}
```

## See also

- `Fluxzy.Core.NetworkErrorCodes` (source: `src/Fluxzy.Core/Core/NetworkErrorCodes.cs`)
- `ClientError.NetworkErrorCode` (source: `src/Fluxzy.Core/ClientError.cs`)
- `NetworkErrorFilter` selects exchanges with `StatusCode == 528`
(source: `src/Fluxzy.Core/Rules/Filters/ResponseFilters/NetworkErrorFilter.cs`)
155 changes: 87 additions & 68 deletions src/Fluxzy.Core/ClientError.cs
Original file line number Diff line number Diff line change
@@ -1,68 +1,87 @@
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak

using System;
using MessagePack;

namespace Fluxzy
{
/// <summary>
/// Holds information about a client error
/// </summary>
[MessagePackObject]
public class ClientError
{
/// <summary>
/// Create a new instance from error code and message
/// </summary>
/// <param name="errorCode"></param>
/// <param name="message"></param>
public ClientError(int errorCode, string message)
{
ErrorCode = errorCode;
Message = message;
}

/// <summary>
/// OS error code
/// </summary>
[Key(0)]
public int ErrorCode { get; }

/// <summary>
/// Friendly error message
/// </summary>
[Key(1)]
public string Message { get; }

/// <summary>
/// Exception message
/// </summary>
[Key(2)]
public string? ExceptionMessage { get; set; }


protected bool Equals(ClientError other)
{
return ErrorCode == other.ErrorCode && Message == other.Message && ExceptionMessage == other.ExceptionMessage;
}

public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;

if (ReferenceEquals(this, obj))
return true;

if (obj.GetType() != this.GetType())
return false;

return Equals((ClientError)obj);
}

public override int GetHashCode()
{
return HashCode.Combine(ErrorCode, Message, ExceptionMessage);
}
}
}
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak

using System;
using MessagePack;

namespace Fluxzy
{
/// <summary>
/// Holds information about a client error
/// </summary>
[MessagePackObject]
public class ClientError
{
/// <summary>
/// Create a new instance from error code and message
/// </summary>
/// <param name="errorCode"></param>
/// <param name="message"></param>
public ClientError(int errorCode, string message)
{
ErrorCode = errorCode;
Message = message;
}

/// <summary>
/// Create a new instance with an explicit network error code
/// </summary>
public ClientError(int errorCode, string message, string? networkErrorCode)
{
ErrorCode = errorCode;
Message = message;
NetworkErrorCode = networkErrorCode;
}

/// <summary>
/// OS error code
/// </summary>
[Key(0)]
public int ErrorCode { get; }

/// <summary>
/// Friendly error message
/// </summary>
[Key(1)]
public string Message { get; }

/// <summary>
/// Exception message
/// </summary>
[Key(2)]
public string? ExceptionMessage { get; set; }

/// <summary>
/// Stable errno-like identifier for the kind of network failure (e.g. <c>connection_refused</c>,
/// <c>dns_notfound</c>, <c>tls_cert_expired</c>). See <see cref="Fluxzy.Core.NetworkErrorCodes"/>.
/// </summary>
[Key(3)]
public string? NetworkErrorCode { get; set; }


protected bool Equals(ClientError other)
{
return ErrorCode == other.ErrorCode && Message == other.Message
&& ExceptionMessage == other.ExceptionMessage
&& NetworkErrorCode == other.NetworkErrorCode;
}

public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;

if (ReferenceEquals(this, obj))
return true;

if (obj.GetType() != this.GetType())
return false;

return Equals((ClientError)obj);
}

public override int GetHashCode()
{
return HashCode.Combine(ErrorCode, Message, ExceptionMessage, NetworkErrorCode);
}
}
}
26 changes: 21 additions & 5 deletions src/Fluxzy.Core/Clients/Dns/DefaultDnsResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,21 @@ public async Task<IReadOnlyCollection<IPAddress>> SolveDnsAll(string hostName)
catch (Exception ex)
{
var errorCode = -1;
var networkErrorCode = NetworkErrorCodes.DnsFailure;

if (ex is SocketException sex)
if (ex is SocketException sex) {
errorCode = sex.ErrorCode;
networkErrorCode = MapDnsSocketError(sex.SocketErrorCode);
}

var clientErrorException = new ClientErrorException(
errorCode, $"Failed to solve DNS for {hostName}", ex.Message);
errorCode, $"Failed to solve DNS for {hostName}",
innerMessageException: ex.Message,
networkErrorCode: networkErrorCode);

throw clientErrorException;
}

}

public async Task<IPAddress> SolveDns(string hostName)
Expand All @@ -53,7 +58,8 @@ public async Task<IPAddress> SolveDns(string hostName)

if (found == null)
throw new ClientErrorException(-1, $"Failed to solve DNS for {hostName}",
"No IP address found");
innerMessageException: "No IP address found",
networkErrorCode: NetworkErrorCodes.DnsNoData);

return found;
}
Expand All @@ -71,9 +77,19 @@ protected virtual async Task<IEnumerable<IPAddress>> InternalSolveDns(string hos
return await SolveDns(hostName).ConfigureAwait(false);
}
catch {
// it's quiet solving
// it's quiet solving
return null;
}
}

internal static string MapDnsSocketError(SocketError socketError)
{
return socketError switch {
SocketError.HostNotFound => NetworkErrorCodes.DnsNotFound,
SocketError.NoData => NetworkErrorCodes.DnsNoData,
SocketError.TryAgain => NetworkErrorCodes.DnsTryAgain,
_ => NetworkErrorCodes.DnsFailure
};
}
}
}
11 changes: 9 additions & 2 deletions src/Fluxzy.Core/Clients/Dns/DnsOverHttpsResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,20 @@ public DnsOverHttpsResolver(string nameOrUrl, ProxyConfiguration? proxyConfigura

if (dnsResponse == null)
{
throw new ClientErrorException(0, "Invalid DNS response (null)");
throw new ClientErrorException(0, "Invalid DNS response (null)",
networkErrorCode: NetworkErrorCodes.DnsFailure);
}

if (dnsResponse.Status != 0)
{
// RFC 8484 / DNS RCODE 3 = NXDOMAIN
var token = dnsResponse.Status == 3
? NetworkErrorCodes.DnsNotFound
: NetworkErrorCodes.DnsFailure;

throw new ClientErrorException(0,
$"Failed to resolve DNS over HTTPS. Status response = {dnsResponse.Status}");
$"Failed to resolve DNS over HTTPS. Status response = {dnsResponse.Status}",
networkErrorCode: token);
}

var result = dnsResponse.Answers
Expand Down
4 changes: 3 additions & 1 deletion src/Fluxzy.Core/Clients/H11/Http11PoolProcessing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ await ForwardInterimToClient(exchange, earlyStatus, cancellationToken)

throw new ClientErrorException(0,
"The connection was closed while trying to read the response header",
ex.Message, ex);
innerMessageException: ex.Message,
innerException: ex,
networkErrorCode: NetworkErrorCodes.ConnectionClosed);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Fluxzy.Core/Clients/H2/StreamWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,8 @@ public async ValueTask ProcessResponse(CancellationToken cancellationToken, H2Co
}

throw new ClientErrorException(1,
"The connection was interrupted before receiving response header");
"The connection was interrupted before receiving response header",
networkErrorCode: NetworkErrorCodes.ProtocolError);
}
catch (Exception) {
Parent.NotifyDispose(this);
Expand Down
Loading
Loading