asp.net-core-webapihttp-errorangular-fullstackasp.net-core-8angular18

Angular18 client request giving HttpErrorResponse instead of hitting API endpoint after form submission -


I am writing an ASP.NET Core 8 Web API and Angular 18 fullstack app. Currently working on a testing whether my requests from client app are hitting the backend API endpoints or not. I have a registration page where I am filling out the required details and clicking 'Submit'. I was expecting the breakpoint in my API endpoint method to be hit. That didn't happen.

I consoled out the responses I am supposed to receive and found out, I am receiving a HttpResponseError. I also got a net::ERR_CONNECTION_REFUSED error message. I'll list out the details later on in the post. Let me first post the relevant piece of code from the client app and the backend API so you can inspect what I tried so far.

AuthController.cs:

[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
    private readonly UserManager<AppUser> _userManager;
    private readonly IConfiguration _configuration;

    public AuthController(UserManager<AppUser> userManager, IConfiguration configuration)
    {
        _userManager = userManager;
        _configuration = configuration;
    }

    [HttpPost("samplemethod")]
    [AllowAnonymous]
    public ActionResult<string> SampleMethod([FromBody]RegisterViewModel viewModel)
    {
        if (viewModel != null)
        {
            return Ok(new AuthResponseViewModel
                {
                    Token = "blablabla",
                    Result = true,
                    Message = "Received data from client app!"
                });
        }
        else
        {
            return BadRequest(new AuthResponseViewModel
                {
                    Token = " ",
                    Result = false,
                    Message = "Did not receive data from client app!"
                });
        }
    }
}

RegisterViewModel.cs:

public class RegisterViewModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Required]
    public string FullName { get; set; } = string.Empty;

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; } = string.Empty;

    [Required]
    [DataType(DataType.Password)]
    public string ConfirmPassword { get; set; } = string.Empty;

    [Required]
    public string MobileNumber { get; set; } = string.Empty;

    [Required]
    public string SubscriptionType { get; set; } = string.Empty;

    [Required]
    public string Role { get; set; } = string.Empty;
}

AuthResponseViewModel.cs:

public class AuthResponseViewModel
{
    public string Token { get; set; } = string.Empty;
    public bool Result { get; set; }
    public string Message { get; set; } = string.Empty;
}

Program.cs :

using ForSideSystems.API.Data;
using ForSideSystems.API.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.SqlServer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Text;
using ForSideSystems.API.Extensions;


var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

builder.Services.AddSwaggerExplorer()
                .InjectDbContext(builder.Configuration)
                .AddAppConfig(builder.Configuration)
                .AddIdentityHandlersAndStores()
                .AddIdentityAuth(builder.Configuration);

var app = builder.Build();

app.ConfigureCORS(builder.Configuration)
   .ConfigureSwaggerExplorer()
   .AddIdentityAuthMiddlewares();

app.UseDefaultFiles();
app.UseStaticFiles();


app.MapControllers();

app.Run();

AppConfigExtensions.cs : *** contains the CORS configuration

public static class AppConfigExtensions
{
    public static WebApplication ConfigureCORS(this WebApplication app, IConfiguration config) 
    {
        app.UseCors(t =>
        t.WithOrigins("http://localhost:4200")
        .AllowAnyMethod()
        .AllowAnyHeader());

        return app;
    }

    public static IServiceCollection AddAppConfig(this IServiceCollection services, IConfiguration config)
    {
        services.Configure<AuthSettings>(config.GetSection("AuthSettings"));
        return services;
    }
}

Now code block from my Angular 18 client app.

auth.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';


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

  constructor(private http:HttpClient) { }

  authURL = 'http://localhost:5254/api/auth';

  createUser(formData:any){
    return this.http.post(this.authURL+'/register', formData);
  }

  createSampleResponse(formData:any)
  {
    console.log("sending request to backend api");
    return this.http.post(this.authURL+'/samplemethod', formData);
  }
  
}

registration.component.html -

<div class="mb-4 h2 text-success">
    Sign up
</div>

<form [formGroup]="regform" (ngSubmit)="onSubmit()">
    <div class="mb-3">
        <input class="form-control bg-body-secondary" placeholder="Email" formControlName="email">
        <div class="error-feedback" *ngIf="hasDisplayableError('email')">
          <div *ngIf="regform.controls.email.hasError('required')">
            Please enter email address.
          </div>
          <div *ngIf="regform.controls.email.hasError('email')">
            Please enter a valid email address.
          </div>
        </div>
      </div>

    <div class="mb-3">
        <input class="form-control bg-body-secondary" placeholder="Full Name" formControlName="fullName">
        <div class="error-feedback" *ngIf="hasDisplayableError('fullName') && regform.controls.fullName.hasError('required')">
          Please enter full name.
        </div>
    </div>

    <div class="mb-3">
        <input class="form-control bg-body-secondary" placeholder="Password" formControlName="password">
        <div class="error-feedback" *ngIf="hasDisplayableError('password')">
          <ng-container [ngSwitch]="regform.controls.password.errors | firstKey">
            <div *ngSwitchCase="'required'">
              Please enter password.
            </div>
            <div *ngSwitchCase="'minlength'">
              Atleast 6 characters required.
            </div>
            <div *ngSwitchCase="'pattern'">
              One or more special character(s).
            </div>
          </ng-container>
          
        </div>
    </div>

    <div class="mb-3">
        <input class="form-control bg-body-secondary" placeholder="Confirm Password" formControlName="confirmPassword">
        <div class="error-feedback" 
        *ngIf="hasDisplayableError('confirmPassword') && regform.controls.confirmPassword.hasError('passwordMismatch')">
          Passwords doesn't match.
        </div>
    </div>

    <div class="mb-3">
      <input class="form-control bg-body-secondary" placeholder="Mobile Number" formControlName="mobileNumber">
      <div class="error-feedback"
      *ngIf="hasDisplayableError('mobileNumber') && regform.controls.mobileNumber.hasError('required')">
        Please provide 10 digit mobile number
      </div>
    </div>

    <div class="mb-3">
      <select class="form-control bg-body-secondary" formControlName="subType" (change)="changeSubscription($event)">
        <option value="">-- Choose Subscription Type --</option>
        <option value="Monthly">Monthly</option>
        <option value="Quarterly">Quarterly</option>
        <option value="Yearly">Yearly</option>
      </select>
      <div class="error-feedback" *ngIf="isSubmitted && f.subType.errors">
        <div *ngIf="f.subType.errors.required">Please select Subscription Type</div>
      </div>
    </div>

    <div class="mb-3">
      <select class="form-control bg-body-secondary" formControlName="role" (change)="changeRole($event)">
        <option value="">-- Choose Role --</option>
        <option value="Actor">Actor</option>
        <option value="Actor">Painter</option>
        <option value="Actor">Photographer</option>
        <option value="Actor">Musician</option>
      </select>
      <div class="error-feedback" *ngIf="isSubmitted && f.role.errors">
        <div *ngIf="f.role.errors.required">Please select Role Type</div>
      </div>
    </div>
    
    <div class="mt-4">
        <button type="submit" class="btn btn-success w-100 rounded-3">
          Register
        </button>
    </div>
</form>

registration.component.ts -

import { Component } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { CommonModule } from '@angular/common';
import { AbstractControl, FormBuilder, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';
import { FirstKeyPipe } from '../../shared/pipes/first-key.pipe';
import { AuthService } from '../../shared/services/auth.service';

@Component({
  selector: 'app-registration',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule, FirstKeyPipe],
  templateUrl: './registration.component.html',
  styles: ``
})

export class RegistrationComponent {

  /**
   *
   */
  constructor(public formBuilder: FormBuilder, private toastr: ToastrService, private service: AuthService,) {

  }

  isSubmitted: boolean = false;

  passwordMatchValidator: ValidatorFn = (control: AbstractControl): null => {
    const password = control.get('password')
    const confirmPassword = control.get('confirmPassword')

    if (password && confirmPassword && password.value != confirmPassword.value)
      confirmPassword?.setErrors({ passwordMismatch: true })
    else
      confirmPassword?.setErrors(null)

    return null;
  }

  regform = this.formBuilder.group({
    fullName: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [
      Validators.required,
      Validators.minLength(6),
      Validators.pattern(/(?=.*[^a-zA-Z0-9 ])/)]],
    confirmPassword: [''],
    mobileNumber:['', Validators.required],
    subType:['', Validators.required],
    role:['', Validators.required]
  }, { validators: this.passwordMatchValidator })

  hasDisplayableError(controlName: string): Boolean {
    const control = this.regform.get(controlName);
    return Boolean(control?.invalid) &&
      (this.isSubmitted || Boolean(control?.touched)|| Boolean(control?.dirty))
  }

  onSubmit()
  {
    this.isSubmitted=true;

    if(this.regform.valid)
    {
      this.service.createSampleResponse(this.regform.value)
        .subscribe({
          next:(res:any)=>
          {
            if(res.result)
            {
              this.regform.reset();
              this.isSubmitted = false;

              this.toastr.success('Response received from server backend!', 'Response')
            }
          },
          error:err=>
          {
            console.log('error:', err);
          }
        })
    }
  }

  get f() {
    return this.regform.controls;
  }

  changeSubscription(e:any) {
    this.regform.controls.subType.setValue(e.target.value, {
      onlySelf: true
    })
  }

  changeRole(e:any)
  {
    this.regform.controls.role.setValue(e.target.value, {
      onlySelf: true
    })
  }

}

first-key.pipe.ts - ** custom pipe created for input validation in reactive forms

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'firstKey',
  standalone: true
})
export class FirstKeyPipe implements PipeTransform {

  transform(value: any): string | null {
    const keys = Object.keys(value);
    if (keys && keys.length > 0)
      return keys[0];
    return null;
  }

}

Finally the error response I am receiving on console -

registration.component.ts:65 
 POST http://localhost:5254/api/auth/samplemethod 400 (Bad Request)

registration.component.ts:78 error: 
HttpErrorResponse {headers: _HttpHeaders, status: 400, statusText: 'Bad Request', url: 'http://localhost:5254/api/auth/samplemethod', ok: false, …}
error
: 
errors
: 
{SubscriptionType: Array(1)}
status
: 
400
title
: 
"One or more validation errors occurred."
traceId
: 
"00-634b9f748dc9bdc47bc227a19c2ddf7a-596e55061b94d743-00"
type
: 
"https://tools.ietf.org/html/rfc9110#section-15.5.1"
[[Prototype]]
: 
Object
headers
: 
_HttpHeaders {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}
message
: 
"Http failure response for http://localhost:5254/api/auth/samplemethod: 400 Bad Request"
name
: 
"HttpErrorResponse"
ok
: 
false
status
: 
400
statusText
: 
"Bad Request"
url
: 
"http://localhost:5254/api/auth/samplemethod"
[[Prototype]]
: 
HttpResponseBase



POST http://localhost:5254/api/auth/samplemethod 
net::ERR_CONNECTION_REFUSED

That's that. The error stack says 'One or more validation errors occurred.' But I am sure I am sending proper validated data from the front end. So this makes no sense. I'm puzzled. My client app is Angular18 standalone type application. I tried adding and removing the [FromBody] from my 'SampleMethod' api endpoint just to make sure that's not what's causing it. Nope, no clue there either. So what could be causing this? Any ideas? Where should I be debugging/looking for in order to locate the exact source of this problem? I need some help guys, been stuck with this for 2 straight days now!

Let me know if you need any more code snippets(interconnected) for analysing the scenario. Would be glad to share.

Thanks in anticipation!


Solution

  • I resolved this thing. After careful inspection, I found the source of error in the registration.component.ts file, in the section where I am initializing the fields inside the regform form group.

    I used this line

    subType:['', Validators.required]
    

    Instead it should be

    subscriptionType:['', Validators.required]
    

    Because in my RegistrationViewModel.cs file, the respective field name is SubscriptionType. The anomaly in the field names was causing the post response to fail.

    It's working fine now. A matter of close details!