96 lines
4.4 KiB
C#
96 lines
4.4 KiB
C#
using System.Diagnostics;
|
||
using Fleet.Core;
|
||
using Microsoft.AspNetCore.Http;
|
||
using MoShiro.Backend.Application;
|
||
using MoShiro.Protocol;
|
||
|
||
namespace MoShiro.Backend.Api;
|
||
|
||
/// <summary>错误码到 HTTP 状态的映射:只在 Api 层做一次。</summary>
|
||
public static class ErrorCodeHttpMap
|
||
{
|
||
// 必须声明在 s_map 之前:静态字段按文本顺序初始化,BuildMap 会用到它
|
||
public static readonly ErrorCode NotImplemented = new("not_implemented");
|
||
|
||
private static readonly Dictionary<string, int> s_map = BuildMap();
|
||
|
||
public static int StatusOf(ErrorCode code) => s_map.GetValueOrDefault(code.Value, StatusCodes.Status500InternalServerError);
|
||
|
||
public static string TitleOf(int status) => status switch
|
||
{
|
||
400 => "Validation failed",
|
||
401 => "Unauthorized",
|
||
403 => "Forbidden",
|
||
404 => "Not found",
|
||
409 => "Conflict",
|
||
410 => "Gone",
|
||
412 => "Precondition failed",
|
||
413 => "Payload too large",
|
||
422 => "Unprocessable request",
|
||
429 => "Too many requests",
|
||
501 => "Not implemented",
|
||
503 => "Service unavailable",
|
||
_ => "Internal error",
|
||
};
|
||
|
||
private static Dictionary<string, int> BuildMap()
|
||
{
|
||
var map = new Dictionary<string, int>(StringComparer.Ordinal);
|
||
void Add(int status, params ErrorCode[] codes)
|
||
{
|
||
foreach (ErrorCode code in codes)
|
||
{
|
||
map[code.Value] = status;
|
||
}
|
||
}
|
||
|
||
Add(400, MoShiroErrors.ValidationFailed, MoShiroErrors.BudgetExceeded, FleetErrors.InvalidParams);
|
||
Add(401, MoShiroErrors.Unauthorized, MoShiroErrors.CsrfFailed, FleetErrors.Unauthorized);
|
||
Add(403, MoShiroErrors.Forbidden, MoShiroErrors.SitePrivate, FleetErrors.Forbidden, FleetErrors.ProofInvalid);
|
||
Add(
|
||
404, MoShiroErrors.NotFound, MoShiroErrors.AgentNotFound, MoShiroErrors.ProbeNotFound, MoShiroErrors.TaskNotFound, MoShiroErrors.TestNotFound, MoShiroErrors.ChunkNotFound,
|
||
MoShiroErrors.TokenNotFound, MoShiroErrors.UnknownKind, MoShiroErrors.UnknownPlan, FleetErrors.NotFound);
|
||
Add(409, MoShiroErrors.NameConflict, MoShiroErrors.ProbeRevoked, MoShiroErrors.ProbeOffline, MoShiroErrors.TestBusy, MoShiroErrors.AgentNotActive, MoShiroErrors.AgentOffline, FleetErrors.NameConflict, FleetErrors.NodeOffline);
|
||
Add(410, MoShiroErrors.RawExpired, MoShiroErrors.JoinTokenExpired, FleetErrors.JoinTokenExpired);
|
||
Add(412, MoShiroErrors.PreconditionFailed);
|
||
Add(413, MoShiroErrors.PayloadTooLarge);
|
||
Add(422, MoShiroErrors.ProbeRequired, MoShiroErrors.RangeTooLarge, MoShiroErrors.InvalidStep, MoShiroErrors.InvalidRange, MoShiroErrors.UnknownMetric, MoShiroErrors.UnknownPreset, MoShiroErrors.TooManyMetrics, FleetErrors.Unsupported);
|
||
Add(429, MoShiroErrors.TooManyRequests, MoShiroErrors.TooManyStreams);
|
||
Add(503, MoShiroErrors.StorageUnavailable, FleetErrors.Timeout);
|
||
map[NotImplemented.Value] = 501;
|
||
return map;
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>所有 4xx/5xx 响应都使用 problem+json;5xx 不泄露异常信息,只提供 trace_id。</summary>
|
||
public static class Problems
|
||
{
|
||
public const string ContentType = "application/problem+json";
|
||
|
||
public static IResult From(HttpContext http, FleetError error)
|
||
{
|
||
int status = ErrorCodeHttpMap.StatusOf(error.Code);
|
||
IReadOnlyDictionary<string, string[]>? fields = error is ValidationError v
|
||
? v.Fields.ToDictionary(f => f.Key, f => new[] { f.Value })
|
||
: null;
|
||
return Create(http, status, error.Code.Value, status >= 500 ? null : error.Message, fields);
|
||
}
|
||
|
||
public static IResult Create(HttpContext http, ErrorCode code, string? detail) =>
|
||
Create(http, ErrorCodeHttpMap.StatusOf(code), code.Value, detail, null);
|
||
|
||
public static IResult Create(HttpContext http, int status, string code, string? detail, IReadOnlyDictionary<string, string[]>? errors = null)
|
||
{
|
||
string? trace = Activity.Current?.Id ?? http.TraceIdentifier;
|
||
var problem = new ProblemDto("about:blank", ErrorCodeHttpMap.TitleOf(status), status, code, detail, trace, errors);
|
||
return TypedResults.Json(problem, ApiJsonContext.Default.ProblemDto, ContentType, status);
|
||
}
|
||
|
||
public static IResult RetryAfter(HttpContext http, FleetError error, int seconds)
|
||
{
|
||
http.Response.Headers.RetryAfter = seconds.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||
return From(http, error);
|
||
}
|
||
}
|