اوه! خطایی که توانست به صورت مخفیانه وارد تولید شود، چه کار کنیم؟
یک صفحه خطای خوب می تواند یک تجربه کاربری ناخوشایند را به چیزی بهتر تبدیل کند، حتی ممکن است باعث خنده شود. در این مقاله، ابزارهایی را که .NET 6 و Umbraco 10 ارائه میدهند بررسی میکنیم و یک صفحه خطا ایجاد میکنیم که میتواند توسط ویرایشگرهای محتوا با یک بازگشت ثابت ویرایش شود، اگر اوضاع واقعاً بد باشد.
کاری که قراره انجام بدیم
راه اندازی صفحات خطای پویا دشوار نیست، اما چند مرحله طول می کشد. اگر این آموزش را دنبال کنید، موارد زیر را انجام خواهید داد:
- برای ورود مجدد به خط لوله میان افزار در صورت خطا، یک کنترل کننده استثنا اضافه کنید
- یک محتوا یاب ایجاد کنید تا صفحه خطای Umbraco را در هنگام ورود مجدد پیدا کنید
- یک میان افزار اضافی ایجاد کنید تا پس از ورود مجدد، زمینه را پاکسازی کنید
- یک میانافزار کنترلکننده استثنا اضافه کنید که در صورت شکست صفحه خطای پویا به صفحه خطای ثابت بازمیگردد.
- یک میان افزار اضافی ایجاد کنید تا مطمئن شوید که صفحه خطای استاتیک با کد وضعیت 500 ارائه می شود
قبل از اینکه شروع کنیم
برای اهداف تصویری، ما به سرعت یک صفحه را می شکنیم، بنابراین می توانیم ببینیم که میان افزار خطا چگونه عمل می کند. من قصد دارم یک وب سایت اولیه ایجاد کنم و کیت شروع تمیز را نصب کنم. سپس مسیر صفحه تماس را با این کنترلر ربودم:
ContactController.cs
public class ContactController : RenderController
{
public ContactController(
ILogger<RenderController> logger,
ICompositeViewEngine compositeViewEngine,
IUmbracoContextAccessor umbracoContextAccessor)
: base(logger, compositeViewEngine, umbracoContextAccessor)
{
}
public override IActionResult Index()
{
throw new Exception("Whoops, something went wrong!");
}
}
حال اگر صفحه تماس را درخواست کنیم، صفحه خطای استاندارد (زشت) 500 را دریافت می کنیم:
مرحله 1: کنترل کننده استثنا را اضافه کنید
ASP.NET 6 یک میان افزار اختصاصی برای رسیدگی به استثناها دارد. این به شما اجازه میدهد تا با یک آدرس اینترنتی دیگر وارد خط لوله میانافزار شوید و به شما امکان میدهد در صورت بروز خطا، صفحه دیگری را ارائه دهید. در مورد ما، مهم نیست که آدرس اینترنتی چیست. کنترل کننده را در خط لوله میان افزار خود به این صورت اضافه کنید:
Startup.cs
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... other middlewares
// 👇 Insert this middleware before the Umbraco middlewares.
// The path that you enter here can be anything you want, but it cannot be a path with a file extension.
app.UseExceptionHandler("/error");
app.UseUmbraco()
.WithMiddleware(u =>
{
// ... The rest of the pipeline
}
}
}
تبریک میگوییم، اکنون میتوانید پس از یک استثنا دوباره وارد خط لوله میانافزار شوید.
در مرحله بعد، باید به Umbraco بگوییم که چگونه محتوای مناسب برای صفحه خطا را پیدا کند:
مرحله 2: ایجاد یک محتوا یاب
Umbraco یک راهحل مسیریابی گسترده دارد که میتوانیم آن را برای انتخاب صفحه خطا برای رسیدگی به خطاها انتخاب کنیم. ابتدا یک صفحه خطای سرور اختصاصی ایجاد می کنیم و سپس یک محتوا یاب ایجاد می کنیم که بتواند آن را پیدا کند:
من قصد دارم از نوع سند خطا کپی کنم و آن را “خطای سرور” نام ببرم:
سپس یک صفحه خطای جدید در درخت محتوا درست زیر صفحه اصلی ایجاد می کنم:
اکنون که محتوایی برای نمایش در صورت خطا داریم، میتوانیم یک محتوا یاب ایجاد کنیم تا آن را برای ما پیدا کند:
ServerErrorContentFinder.cs
public class ServerErrorContentFinder : IContentFinder
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
public ServerErrorContentFinder(IHttpContextAccessor httpContextAccessor, IUmbracoContextAccessor umbracoContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_umbracoContextAccessor = umbracoContextAccessor;
}
public Task<bool> TryFindContent(IPublishedRequestBuilder request)
{
// 👇 Upon re-entry, the exception middleware adds a feature to the context with the details about the exception.
// This let's us detect if we're handling errors currently or not.
if (_httpContextAccessor.GetRequiredHttpContext().Features.Get<IExceptionHandlerPathFeature>() is null)
{
return Task.FromResult(false);
}
// at this point, we know that we are routing an error page
var rootNode = GetRootNode(request);
// 👇 In our example, the server error page is always below the homepage and of type ServerError,
// but you can use any logic you want to find the error page.
var errorPage = rootNode.FirstChild<ServerError>();
request.SetPublishedContent(errorPage);
return Task.FromResult(true);
}
private IPublishedContent GetRootNode(IPublishedRequestBuilder request)
{
// 👇 Umbraco provides us with the appropriate root node automatically, so we can use that to select the root node.
var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
if (request.HasDomain())
{
return umbracoContext.Content!.GetById(request.Domain!.ContentId)!;
}
// 👇 if no domain is assigned, we have to fall back to the first node in the content root
return umbracoContext.Content!.GetAtRoot().First();
}
}
سپس محتوا یاب را در ظرف DI با استفاده از یک آهنگساز ثبت می کنیم:
ServerErrorComposer.cs
public class ServerErrorComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
// 👇 Insert the content finder at the start so it has a chance to route the request before other content finders do.
builder.ContentFinders().Insert<ServerErrorContentFinder>(0);
}
}
پس همین بود؟ نه کاملا. ما هنوز صفحه زشت 500 را دریافت می کنیم و متوجه خواهید شد که یاب محتوا هرگز بر اساس خطا فعال نمی شود. دلیل این امر این است که Umbraco مقداری قلوه سنگ در پشت خود باقی می گذارد که مانع از اجرای مجدد مرحله مسیریابی می شود. ما باید زمینه را تمیز کنیم تا یاب محتوا کار کند.
مرحله 3: زمینه را برای رسیدگی به خطا آماده کنید
پس از مسیریابی یک درخواست، Umbraco یک شی با مقادیر مسیری که محاسبه کرده است ایجاد می کند. تا زمانی که آن شی وجود دارد، Umbraco از مسیریابی مجدد درخواست خودداری خواهد کرد. راه حل این است که به سادگی شی را پس از ورود مجدد حذف کنید.
علاوه بر این، میانافزار کنترلکننده استثنا، مسیر درخواست را به هر چیزی که در مرحله 1 وارد کردهایم تغییر میدهد. برای اینکه یاب محتوای ما مطابق با هدف کار کند، باید آن مسیر را به طور موقت به حالت اصلی بازنشانی کنیم.
برای انجام این کار، میان افزار زیر را ایجاد می کنیم:
ServerErrorCleanupMiddleware.cs
public class ServerErrorCleanupMiddleware
{
private readonly RequestDelegate _next;
public ServerErrorCleanupMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 👇 Only run this middleware if we're handling errors
var errorRoutingFeature = context.Features.Get<IExceptionHandlerPathFeature>();
if (errorRoutingFeature is null)
{
await _next(context);
return;
}
// 👇 Delete the Umbraco route values so that umbraco will recalculate them
context.Features.Set<UmbracoRouteValues>(null);
// 👇 If the error path is set to a static file,
// we don't want to apply any further cleanup and should skip the rest of this middleware.
if (context.Request.IsClientSideRequest())
{
await _next(context);
return;
}
// 👇 The error feature contains the original request path.
// Umbraco will automatically calculate the umbraco domain for us if we reset the path
var originalPath = context.Request.Path;
context.Request.Path = errorRoutingFeature.Path;
try
{
await _next(context);
}
finally
{
// 👇 after running the middleware, make sure to restore the path to the error path.
// Otherwise the exception middleware gets stuck in a loop
context.Request.Path = originalPath;
}
}
}
اکنون این میان افزار را در خط لوله میان افزار، درست در پشت میان افزار کنترل کننده استثنا ثبت کنید:
Startup.cs
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... other middlewares
app.UseExceptionHandler("/error");
app.UseMiddleware<ServerErrorCleanupMiddleware>();
app.UseUmbraco()
.WithMiddleware(u =>
{
// ... The rest of the pipeline
}
}
}
اکنون صفحه تماس را بررسی می کنیم:
کار می کند!! 🎉
مرحله 4: یک صفحه خطای بازگشتی ثابت اضافه کنید
اما اگر صفحه خطا نیز از کار بیفتد چه؟ بیایید این را با قرار دادن یک استثنا در صفحه خطا شبیه سازی کنیم:
ServerErrorController.cs
public class ServerErrorController : RenderController
{
public ServerErrorController(
ILogger<RenderController> logger,
ICompositeViewEngine compositeViewEngine,
IUmbracoContextAccessor umbracoContextAccessor)
: base(logger, compositeViewEngine, umbracoContextAccessor)
{
}
public override IActionResult Index()
{
throw new Exception("Oof, the error page doesn't work!");
}
}
حالا با یک صفحه 500 زشت به همان جایی که شروع کردیم برگشتیم. برای حل این مشکل، یک میان افزار کنترل کننده استثنا دیگر را درست قبل از اولی اضافه می کنیم:
Startup.cs
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... other middlewares
// 👇 Add another exception handler here that points to a static file
app.UseExceptionHandler("/error.html");
app.UseExceptionHandler("/error");
app.UseMiddleware<ServerErrorCleanupMiddleware>();
app.UseUmbraco()
.WithMiddleware(u =>
{
// ... The rest of the pipeline
}
}
}
همچنین باید یک فایل استاتیک به پروژه خود اضافه کنیم. ما یک فایل ساده ایجاد می کنیم error.html
داخل wwwroot
:
error.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>500 Server Error</title>
</head>
<body>
<h1>Oops! This page doesn't seem to work right now</h1>
</body>
</html>
اگر اکنون بخواهیم صفحه تماس را بارگیری کنیم، این را می بینیم:
بدیهی است که شما یک صفحه خطای استاتیک جذاب تری نسبت به این صفحه ایجاد می کنید، اما نکته را متوجه شدید.
اگرچه هنوز کاملاً انجام نشده است، زیرا اگر به DevTools نگاه کنیم، میبینیم که صفحه خطای استاتیک با یک پاسخ OK 200 ارائه میشود.
این مطلوب نیست، زیرا به این معنی است که گوگل ممکن است صفحه خطای شما را ایندکس کند. صفحه خطا باید با کد وضعیت 500 ارائه شود، بنابراین بیایید آن را برطرف کنیم:
مرحله 5: اطمینان از کد وضعیت 500 در صفحات خطا
برای اطمینان از کد وضعیت صحیح در صفحات خطا، میان افزار دیگری ایجاد می کنیم:
ServerErrorResponseCodeMiddleware.cs
public class ServerErrorResponseCodeMiddleware
{
private readonly RequestDelegate _next;
public ServerErrorResponseCodeMiddleware(RequestDelegate next)
{
_next = next;
}
public Task InvokeAsync(HttpContext context)
{
// 👇 As soon as we start writing the response to the client,
// we need to check if we're sending an error response.
// If we do: ensure that the statuscode is 500
context.Response.OnStarting(() =>
{
var exceptionPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionPathFeature is not null)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
return Task.CompletedTask;
});
return _next(context);
}
}
ما این میان افزار را در خط لوله ثبت می کنیم:
Startup.cs
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... other middlewares
// 👇 Add the middleware right before all other error middlewares
app.UseMiddleware<ServerErrorResponseCodeMiddleware>();
app.UseExceptionHandler("/error.html");
app.UseExceptionHandler("/error");
app.UseMiddleware<ServerErrorCleanupMiddleware>();
app.UseUmbraco()
.WithMiddleware(u =>
{
// ... The rest of the pipeline
}
}
}
اکنون صفحه خطا همیشه با کد وضعیت مناسب ارائه می شود:
پیروزی!! 🎉
مزایا
اگر تمام راه را به اینجا رساندی، آفرین! این کمی کار بوده است، اما همه چیز ارزشش را دارد، زیرا ما از چند جهت سود می بریم:
- ✅ مرورگر در همان URL باقی می ماند
- ✅ مرورگر همیشه در صورت خرابی کد وضعیت 500 را دریافت می کند
- ✅ صفحه خطا می تواند هر صفحه ای که می خواهید باشد
- ✅ می توانید از مسیر ربایی Umbraco برای صفحه خطای خود استفاده کنید
- ✅ بازگشت استاتیک یک صفحه خطای خوب را تضمین می کند، مهم نیست که چه باشد
- ✅ بدون توجه به استراتژی دامنه ای که استفاده می کنید، از چند سایت و چند زبان پشتیبانی می کند
افکار نهایی
هر دو ASP.NET 6 و Umbraco 10 یک چارچوب عالی برای ارائه صفحات خطای پویا ارائه می دهند. با چند میان افزار، می توانیم هر صفحه ای را با آزادی تقریبا بی حد و حصر به صفحه خطا تبدیل کنیم.
در حالت ایدهآل، مایلم که این به بستهای تبدیل شود که به ویرایشگرهای محتوا امکان میدهد صفحات خطا را انتخاب کنند، دقیقاً مانند بسته PageNotFound، اما در حال حاضر خوشحالم که فقط از این قطعه کد در پروژههایم استفاده میکنم.
این تمام چیزی است که فعلاً باید بگویم. امیدوارم مفید بوده باشد و شاید شما را در وبلاگ بعدی خود ببینم! 😊