Skip to content

Commit

Permalink
End of section 7 (Error handling)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jovan Arsov committed Apr 11, 2022
1 parent f25322c commit 06b6439
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 14 deletions.
52 changes: 52 additions & 0 deletions API/Controllers/BuggyController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers
{
public class BuggyController : BaseApiController
{
private readonly DataContext _context;
public BuggyController(DataContext context)
{
_context = context;
}

[Authorize]
[HttpGet("auth")]
public ActionResult<string> GetSecret()
{
return "secret text";
}

[HttpGet("not-found")]
public ActionResult<AppUser> GetNotFound()
{
var thing = _context.Users.Find(-1);

if(thing == null) return NotFound();

return Ok();
}

[HttpGet("server-error")]
public ActionResult<string> GetServerError()
{
var thing = _context.Users.Find(-1);

var thingToReturn = thing.ToString();

return thingToReturn;
}

[HttpGet("bad-request")]
public ActionResult<string> GetBadRequest(){
return BadRequest("This was not a good request");
}
}
}
1 change: 1 addition & 0 deletions API/DTOs/RegisterDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class RegisterDto
public string Username { get; set; }

[Required]
[StringLength(8, MinimumLength = 4)]
public string Password { get; set; }
}
}
21 changes: 21 additions & 0 deletions API/Errors/ApiException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace API.Errors
{
public class ApiException
{
public ApiException(int statusCode, string message = null, string details = null)
{
StatusCode = statusCode;
Message = message;
Details = details;
}

public int StatusCode { get; set; }
public string Message { get; set; }
public string Details { get; set; }
}
}
49 changes: 49 additions & 0 deletions API/Middleware/ExceptionMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using API.Errors;

namespace API.Middleware
{
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
private readonly IHostEnvironment _env;

public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger,
IHostEnvironment env)
{
_env = env;
_logger = logger;
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;

var response = _env.IsDevelopment()
? new ApiException(context.Response.StatusCode, ex.Message, ex.StackTrace?.ToString())
: new ApiException(context.Response.StatusCode, "Internal server error");

var options = new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase};

var json = JsonSerializer.Serialize(response, options);

await context.Response.WriteAsync(json);
}
}
}
}
10 changes: 3 additions & 7 deletions API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using API.Data;
using API.Extensions;
using API.Interfaces;
using API.Middleware;
using API.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
Expand Down Expand Up @@ -47,13 +48,8 @@ public void ConfigureServices(IServiceCollection services)
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIv5 v1"));
}

app.UseMiddleware<ExceptionMiddleware>();

app.UseHttpsRedirection();

app.UseRouting();
Expand Down
Binary file added API/datingapp.db-shm
Binary file not shown.
Empty file added API/datingapp.db-wal
Empty file.
57 changes: 57 additions & 0 deletions client/src/app/_interceptors/error.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { NavigationExtras, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

constructor(private router: Router, private toastr: ToastrService) {}

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError(error =>{
if(error){
switch (error.status) {
case 400:
if(error.error.errors){
const modalStateErrors = [];
for(const key in error.error.errors){
if(error.error.errors[key]){
modalStateErrors.push(error.error.errors[key])
}
}
throw modalStateErrors.flat();
}else{
this.toastr.error(error.statusText === "OK" ? "Bad Request" : error.statusText, error.status);
}
break;
case 401:
//this.toastr.error(error.error === null ? "Unauthorized" : error.error, error.status)
this.toastr.error(error.statusText === "OK" ? "Unauthorised" : error.statusText, error.status);
break;
case 404:
this.router.navigateByUrl('/not-found');
break;
case 500:
const navigationExtras: NavigationExtras = {state: {error: error.error}};
this.router.navigateByUrl('/server-error', navigationExtras);
break;
default:
this.toastr.error('Something unexpected went wrong');
console.log(error);
break;
}
}
return throwError(error);
})
)
}
}
8 changes: 7 additions & 1 deletion client/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NotFoundComponent } from './errors/not-found/not-found.component';
import { ServerErrorComponent } from './errors/server-error/server-error.component';
import { TestErrorsComponent } from './errors/test-errors/test-errors.component';
import { HomeComponent } from './home/home.component';
import { ListsComponent } from './lists/lists.component';
import { MemberDetailComponent } from './members/member-detail/member-detail.component';
Expand All @@ -20,7 +23,10 @@ const routes: Routes = [
{path: 'messages', component: MessagesComponent},
]
},
{path: '**', component: HomeComponent, pathMatch: 'full'},
{path: 'errors', component: TestErrorsComponent},
{path: 'not-found', component: NotFoundComponent},
{path: 'server-error', component: ServerErrorComponent},
{path: '**', component: NotFoundComponent, pathMatch: 'full'},
];

@NgModule({
Expand Down
15 changes: 12 additions & 3 deletions client/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
Expand All @@ -16,6 +16,10 @@ import { ListsComponent } from './lists/lists.component';
import { MessagesComponent } from './messages/messages.component';
import { ToastrModule } from 'ngx-toastr';
import { SharedModule } from './_modules/shared.module';
import { TestErrorsComponent } from './errors/test-errors/test-errors.component';
import { ErrorInterceptor } from './_interceptors/error.interceptor';
import { NotFoundComponent } from './errors/not-found/not-found.component';
import { ServerErrorComponent } from './errors/server-error/server-error.component';


@NgModule({
Expand All @@ -27,7 +31,10 @@ import { SharedModule } from './_modules/shared.module';
MemberListComponent,
MemberDetailComponent,
ListsComponent,
MessagesComponent
MessagesComponent,
TestErrorsComponent,
NotFoundComponent,
ServerErrorComponent
],
imports: [
BrowserModule,
Expand All @@ -37,7 +44,9 @@ import { SharedModule } from './_modules/shared.module';
FormsModule,
SharedModule
],
providers: [],
providers: [
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Empty file.
4 changes: 4 additions & 0 deletions client/src/app/errors/not-found/not-found.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="container">
<h1>Not found</h1>
<button class="btn btn-info btn-lg" routerLink="/">Return to home page</button>
</div>
15 changes: 15 additions & 0 deletions client/src/app/errors/not-found/not-found.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-not-found',
templateUrl: './not-found.component.html',
styleUrls: ['./not-found.component.css']
})
export class NotFoundComponent implements OnInit {

constructor() { }

ngOnInit(): void {
}

}
Empty file.
15 changes: 15 additions & 0 deletions client/src/app/errors/server-error/server-error.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<h4>Internal server error - refreshing the page will make the error disappear!</h4>
<ng-container *ngIf="error">
<h5 class="text-danger">Error: {{error.message}}</h5>
<p class="font-weight-bold">Note: If you are seeing this, then Angular is probably not to blame</p>
<p>What to do next?</p>
<ol>
<li>Open the chrome dev tools</li>
<li>Inspect the network tab </li>
<li>Check the failing request</li>
<li>Examing the request URL - make sure it is correct</li>
<li>Reproduse the error in Postman - if we see the same response then the issue is not with Angular</li>
</ol>
<p>Following is the stack trace - this is where your investigation should start!</p>
<code class="mt-5" style="background-color: whitesmoke;">{{error.details}}</code>
</ng-container>
20 changes: 20 additions & 0 deletions client/src/app/errors/server-error/server-error.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
selector: 'app-server-error',
templateUrl: './server-error.component.html',
styleUrls: ['./server-error.component.css']
})
export class ServerErrorComponent implements OnInit {
error: any;

constructor(private router: Router) {
const navigation = this.router.getCurrentNavigation();
this.error = navigation?.extras?.state?.error;
}

ngOnInit(): void {
}

}
Empty file.
15 changes: 15 additions & 0 deletions client/src/app/errors/test-errors/test-errors.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<ng-container>
<button (click)="get500Error()" class="btn btn-outline-primary mr-3">Test 500 Error</button>
<button (click)="get400Error()" class="btn btn-outline-primary mr-3">Test 400 Error</button>
<button (click)="get401Error()" class="btn btn-outline-primary mr-3">Test 401 Error</button>
<button (click)="get404Error()" class="btn btn-outline-primary mr-3">Test 404 Error</button>
<button (click)="get400ValidationError()" class="btn btn-outline-primary mr-3">
Test 400 Validation Error</button>
<div class="row mt-5" *ngIf="validationErrors.length > 0">
<ul class="text-danger">
<li *ngFor="let error of validationErrors">
{{error}}
</li>
</ul>
</div>
</ng-container>

1 comment on commit 06b6439

@jovanarsov
Copy link
Owner

@jovanarsov jovanarsov commented on 06b6439 Apr 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error.statusText always returns status OK(), check the switch component in Error.ErrorInterceptor.ts and also see this solution angular/angular#23334

Please sign in to comment.