スキップしてメイン コンテンツに移動

[Angular] Error handling of HttpClient

Intro

Sometimes I got errors when I had accessed to server with HttpClient.
I had wanted to know what should I do, so I tried in some situations.

Returning errors sample (ASP.NET Core)

Because I had wanted to get some kinds of problems, I used Identity.

Startup.cs


using Files;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Models;
using UpgradeSample.Models;
using UpgradeSample.Products;
using UpgradeSample.Users;

public class Startup
{
    private IConfigurationRoot Configuration { get; }
    public Startup(IHostEnvironment env)
    {
...
    }
    public void ConfigureServices(IServiceCollection services)
    {
        // DB Connect
        services.AddDbContext<UpgradeSampleContext>(options =>
            options.UseNpgsql(Configuration["DbConnect"]));
        // Identity
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddUserStore<ApplicationUserStore>()
            .AddEntityFrameworkStores<UpgradeSampleContext>() 
            .AddDefaultTokenProviders();
        // SPA
        services.AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = "clients/dist";
        });
        // DI
        services.AddSingleton<ILocalFileAccessor, LocalFileAccessor>();
        services.AddScoped<IProductDao, ProductDao>();
        services.AddScoped<IProductService, ProductService>();
        services.AddScoped<IUserService, UserService>();

        services.AddRazorPages();
        services.AddControllers();
    }
    public void Configure(IApplicationBuilder app, IHostEnvironment env)
    {
...
        if (env.IsDevelopment() == false)
        {
            app.UseSpaStaticFiles();
        }
        app.UseStaticFiles();
        app.UseCors(AllowedOrigins);
        app.UseRouting();
        // when I tried to redirect what was caused by unauthorized, I would uncomment.
        // app.UseStatusCodePagesWithRedirects("/login");

        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
        app.UseSpa(spa =>
        {
            spa.Options.SourcePath = "clients";
            
            if (env.IsDevelopment())
            {
                spa.UseAngularCliServer(npmScript: "s");
            }
        });
    }
}

ThrowSampleController.cs


using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Files;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using UpgradeSample.Models;

namespace Controllers
{
    public class ThrowSampleController: Controller
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        public ThrowSampleController(SignInManager<ApplicationUser> signInManager)
        {
            _signInManager = signInManager;
        }
        
        // Throw some results

        /* For authentication */
        [Route("/sample-signin")]
        public async Task Signin()
        {
            await _signInManager.PasswordSignInAsync("hello", "world", false, false);
        }
        [Route("/sample-signin")]
        public async Task SignOut()
        {
            await _signInManager.SignOutAsync();
        }
        // For redirection of unauthorized
        [Route("/Account/Login")]
        public IActionResult GetLoginPage()
        {
            return View("~/Views/LoginPage.cshtml");
        }
        // Generate dummy data
        private List<Product> GenerateSampleData()
        {
            return new List<Product>
            {
                new Product { ProductId = 0, ProductName = "Hello", LastUpdateDate = DateTime.Now },
                new Product { ProductId = 1, ProductName = "World", LastUpdateDate = DateTime.Now },
            };
        }
    }
}

Get results with HttpClient

Default

If no any probrems were happened, I could get data like below.

ThrowSampleController.cs


...
namespace Controllers
{
    public class ThrowSampleController: Controller
    {
...
        [Route("/throw/ok")]
        public List<Product> GetOk()
        {
            return GenerateSampleData();
        }
    }
}

throw-sample.service.ts


import { Injectable } from '@angular/core';
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
import {Observable, throwError} from "rxjs";
import {catchError, map} from 'rxjs/operators';
import {Product} from "./models/product";
import {UploadResult} from "./file-upload/upload-result";

@Injectable({
  providedIn: 'root'
})
export class ThrowSampleService {
  constructor(private httpClient: HttpClient) { }
  public getOk(): Observable<Array<Product>> {
    return this.httpClient.get<Array<Product>>(
      `http://localhost:5000/throw/ok`);
  }
...
}

If I had subscribed this method, I could get the array of "Product".
It was great simple:)
But unfortunately, some errors might happen.

404

ThrowSampleController.cs


...
namespace Controllers
{
    public class ThrowSampleController: Controller
    {
...
        [Route("/throw/notfound")]
        public IActionResult GetNotFound()
        {
            return NotFound();
        }
...
    }
}

throw-sample.service.ts


...
@Injectable({
  providedIn: 'root'
})
export class ThrowSampleService {

  constructor(private httpClient: HttpClient) { }
...
  public getNotFound(): Observable<Array<Product>> {
    return this.httpClient.get<Array<Product>>(
      `http://localhost:5000/throw/notfound`);
  }
  ...
}

This was the result.
Object { headers: {…}, status: 404, statusText: "Not Found", url: "http://localhost:5000/throw/notfound", ok: false, name: "HttpErrorResponse", message: "Http failure response for http://localhost:5000/throw/notfound: 404 Not Found", error: null }
How to handling the errors?
I could write like this.

throw-sample.component.ts


import { Component, OnInit } from '@angular/core';
import {ThrowSampleService} from "../throw-sample.service";

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

  constructor(private throwSampleService: ThrowSampleService) { }
...
  public getNotFound() {
    this.throwSampleService.getNotFound()
      .subscribe(result => console.log(result),
        error => {
            console.error(error);
            // Do something.
        },
        () => console.log('getNotFound completed')*/);
  }
...
}

But should I do like this?
According to the documents, I had felt I shall handle the errors in the service.
Angular - HttpClient

throw-sample.service.ts


...
@Injectable({
  providedIn: 'root'
})
export class ThrowSampleService {
    constructor(private httpClient: HttpClient) { }
...
    public getNotFound(): Observable<Array<Product>> {
        return this.httpClient.get<Array<Product>>(
                `http://localhost:5000/throw/notfound`)
            .pipe(
                catchError(ThrowSampleService.handleError)
            );
    }
...
    private static handleError(error: HttpErrorResponse){
        console.error(error);

        // Do something

        return throwError(error.message);
    }
}

I could write common operations.
For example, if I had got 401 error, I would move to login page.

And I hadn't had to return error in handleError.
So I also could delegate error handling to the class, and return default values.

throw-sample.service.ts


...
@Injectable({
    providedIn: 'root'
})
export class ThrowSampleService {
    constructor(private httpClient: HttpClient) { }
...
  public getNotFound(): Observable<Array<Product>> {
    return this.httpClient.get<Array<Product>>(
            `http://localhost:5000/throw/notfound`)
        .pipe(
            catchError(error => ThrowSampleService.handleError(error, []))
        );
  }
...  
  private static handleError<T> (error: HttpErrorResponse, args: T) {
    console.error(error);
    
    // Do something

    return args;
  }
}

401

How about the other errors?
I tried to handle the 401 error.

ThrowSampleController.cs


...
namespace Controllers
{
    public class ThrowSampleController: Controller
    {
...
        [Route("/throw/unauthorized")]
        public IActionResult GetUnauthorized()
        {
            return Unauthorized();
        }
...
    }
}

throw-sample.service.ts


...
@Injectable({
  providedIn: 'root'
})
export class ThrowSampleService {
  constructor(private httpClient: HttpClient) { }
...
  public getUnauthorized(): Observable<Array<Product>> {
    return this.httpClient.get<Array<Product>>(
      `http://localhost:5000/throw/unauthorized`)
      .pipe(
        catchError(ThrowSampleService.handleError)
      );
  }
...
}

Off cource I could get 401 error.
Object { headers: {…}, status: 401, statusText: "Unauthorized", url: "http://localhost:5000/throw/unauthorized", ok: false, name: "HttpErrorResponse", message: "Http failure response for http://localhost:5000/throw/unauthorized: 401 Unauthorized", error: null }

404 with redirection

How about I had used redirection to the login page?

Startup.cs


...
namespace UpgradeSample
{
    public class Startup
    {
...
        public void Configure(IApplicationBuilder app, IHostEnvironment env)
        {
...
            app.UseStatusCodePagesWithRedirects("/login");
...
        }

ThrowSampleController.cs


...
namespace Controllers
{
    public class ThrowSampleController: Controller
    {
...
        [Route("/throw/unauthorized")]
        public IActionResult GetUnauthorized()
        {
            return Unauthorized();
        }
...
        [Route("/login")]
        [Route("/Account/Login")]
        public IActionResult GetLoginPage()
        {
            return View("~/Views/LoginPage.cshtml");
        }
    }
}

The result was here.
Object { headers: {…}, status: 200, statusText: "OK", url: "http://localhost:5000/login", ok: false, name: "HttpErrorResponse", message: "Http failure during parsing for http://localhost:5000/login", error: {…} }
If the request had been redirected, the status code what I could get was 200.
So I couldn't think only about the status code of 401 in this case.

403

How about 403 error?
I had removed the redirection again.

Startup.cs


...
namespace UpgradeSample
{
    public class Startup
    {
...
        public void Configure(IApplicationBuilder app, IHostEnvironment env)
        {
...
            // app.UseStatusCodePagesWithRedirects("/login");
...
        }

ThrowSampleController.cs


...
namespace Controllers
{
    public class ThrowSampleController: Controller
    {
...
        [Route("/throw/forbidden")]
        public IActionResult GetForbidden()
        {
            return Forbid();        
        }
...
    }
}

throw-sample.service.ts


...
@Injectable({
  providedIn: 'root'
})
export class ThrowSampleService {

  constructor(private httpClient: HttpClient) { }
...
  public getForbidden(): Observable<Array<Product>> {
    return this.httpClient.get<Array<Product>>(
      `http://localhost:5000/throw/forbidden`)
      .pipe(
        catchError(ThrowSampleService.handleError)
      );
  }
...

This was the result.
Object { headers: {…}, status: 404, statusText: "Not Found", url: "http://localhost:5000/Account/AccessDenied?ReturnUrl=%2Fthrow%2Fforbidden", ok: false, name: "HttpErrorResponse", message: "Http failure response for http://localhost:5000/Account/AccessDenied?ReturnUrl=%2Fthrow%2Fforbidden: 404 Not Found", error: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot GET /Account/AccessDenied</pre>\n</body>\n</html>\n" }
Why redirected automatically?
Because I hadn't written the page what was routed "/Account/AccessDenied".
So I hadn't been surprised. But I didn't know the redirection.
I would read the document.

Resources

RxJS in Action
Angular-HttpClient

コメント

このブログの人気の投稿

[Angular][ASP.NET Core] Upload chunked files

Intro I wanted to send files to Web application (made by ASP.NET Core). If the file size had been small, I didn't need do any special things. But when I tried to send a large file, the error was occurred by ASP.NET Core's limitation. Though I could change the settings, but I didn't want to do that, because I hadn't known the file sizes what would been actually using. So I splitted the data into chunks first, and sent them. After receiving all chunks, I merged them into one file. There might be some libraries or APIs (ex. Stream API) what did them automatically, but I couldn't find them. What I did [ASP.NET Core] Make CORS enabled [Angular] Split a large file into chunks [Angular][ASP.NET Core] Send and receive data as form data [ASP.NET Core] Merge chunks into one file [ASP.NET Core] Make CORS enabled Because the client side application(Angular) and the server side application(ASP.NET Core) had been separated, I had to make CORS(Cross-Origin Requests) ...

[Nest.js] Show static files

Intro I wanted to use Nest.js and WebRTC(node-webrtc). NestJS - A progressive Node.js framework Documentation | NestJS - A progressive Node.js framework And because I wanted to try with simple page(not use JavaScript frameworks), I added static HTML, CSS, JavaScript into a Nest.js project. Prepare Install First, I installed @nestjs/cli. First steps | NestJS - A progressive Node.js framework As same as last time , I couldn't do global install because I had used Volta. But I could installed by volta. volta install @nestjs/cli Create project nest new nest-web-rtc-sample volta pin node@12 Run npm start After doing "npm start", I could getting "Hello World!" from http://localhost:3000. Add static files I could add static files by two ways. @nestjs/serve-static First one of them was using "serve-static". Serve Static | NestJS - A progressive Node.js framework npm install --save @nestjs/serve-static And I needed adding a module into app.modu...

[Angular] Sending file with Observable and showing loading screen

Intro When I tried sending file data on last time, I had confused with "promise.then", "async/await" and "Observable". [Angular][ASP.NET Core] Upload chunked files So I wanted to distinct them, and this time, I tried to use "Observable" because HttpClient return Observable<any>. Call observables in order I sended file in these steps. Read file by FileReader Create directory for saving chunks send and saving chunks merge chunks to one file and delete chunks Each steps used the former steps result. So I could write by Promise.then like below. this.executeStep1() // return Promise<UploadResult> .then(result => this.executeStep2(result)) // return Promise<UploadResult> .then(result => this.executeStep3(result)) // return Promise<UploadResult> .catch(reason => console.log(reason)); Result I could write with pipe & flatMap. file-uploader.service.ts public upload(file: File): Observable<U...