Search code examples
c#angularvisual-studio-2017iis-expressangular7

Getting 404 error when making an API call, even though the request count increments


This is completely separate from Cannot find module @angular/core, even though I still have that solution package around in case anyone has ideas.

So I have an Angular7 site and a Web API project in the same solution. I've set IIS Express to use only port 5000; Node will use port 4200. In the Web API project, I've made a new "AccountController" controller, a "LoginAttempt" model, and a "LoginResult" model.

On the Angular side, I have login.component.html, login.component.ts, login.service.ts, and serviceCall.service.ts files. The login.component.ts is updated by the html file and passes the request to the login.service.ts file, which packages things up and sends to the serviceCall.service.ts file to send along to the API.

What happens when the call is attempted is that I'm receiving a 404 error, yet Visual Studio increments the "requests" value assocaited to the Login call. I can't seem to find any reason why I would receive a 404 and still have the request increment on the call attempt.

Source Code: C# Web API: Startup.cs

   public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }

LoginAttempt.cs

[Serializable]
public class LoginAttempt
{
    public string username { get; set; }
    public string password { get; set; }
}

LoginResult.cs

[Serializable]
public class LoginResult
{
    public string token { get; set; }
    public string message { get; set; }
    public bool success { get; set; }
}

AccountController.cs

[Route("api/[controller]")]
public class AccountController : Controller
{
    Account accountRepo = new Account();

    [HttpPost]
    public LoginResult Login(LoginAttempt input)
    {
        return accountRepo.verifyCredentials(input.username, input.password);
    }

    public IActionResult Index()
    {
        return View();
    }
}

Angular7 proxy.conf.json

{
  "exclude": [
    "**/bin",
    "**/bower_components",
    "**/jspm_packages",
    "**/node_modules",
    "**/obj",
    "**/platforms"
  ],
  "/api": {
    "target": "http://localhost:5000",
    "secure": false
  }
}

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "smart-goal": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {},
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/smart-goal",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                }
              ]
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "smart-goal:build",
            "proxyConfig": "proxy.conf.json"
          },
          "configurations": {
            "production": {
              "browserTarget": "smart-goal:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "smart-goal:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "styles": [
              "src/styles.css"
            ],
            "scripts": [],
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "src/tsconfig.app.json",
              "src/tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    },
    "smart-goal-e2e": {
      "root": "e2e/",
      "projectType": "application",
      "prefix": "",
      "architect": {
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "smart-goal:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "smart-goal:serve:production"
            }
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": "e2e/tsconfig.e2e.json",
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    }
  },
  "defaultProject": "smart-goal"
}

LoginCredentials.ts

export class LoginCredentials {
  username: string | undefined;
  password: string | undefined;
}

LoginResults.ts

export interface ILoginResult {
  token: string,
  message: string,
  success: boolean
}

login.component.html

<p>Login Page</p>
<form>
  <label>Username:</label>
  <input type="text" [(ngModel)]="Username" name="Username"/>
  <label>Password:</label>
  <input type="password" [(ngModel)]="Password" name="Password"/>
  <button type="submit" (click)="LoginAttempt()">Submit</button>
</form>

login.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Input } from '@angular/core';
import { LoginCredentials } from '../models/loginCredentials';
import { LoginService } from './login.service';

@Component({
  selector: 'login',
  templateUrl: './login.component.html'
})
export class LoginComponent {
  private router: Router;
  private Username: string;
  private Password: string;
  private Login: LoginCredentials;
  private response: undefined;
  private service: LoginService;

  constructor(router: Router, service: LoginService) {
    this.router = router;
    this.Login = new LoginCredentials();
    this.service = service;
    this.Username = "";
    this.Password = "";
  }

  LoginAttempt() {
    let data = new LoginCredentials();
    data.username = this.Username;
    data.password = this.Password;

    this.service.Login(data)
      .subscribe(
        result => {
          let response = JSON.stringify(result);
          alert("SUCCESS - " + response);
        }
      );
  }
}

login.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { LoginCredentials } from '../models/LoginCredentials';
import { ServiceCall } from '../shared/serviceCall.service';
import { ILoginResult } from '../models/LoginResult';
import { map } from 'rxjs/operators';

@Injectable()
export class LoginService {
  call: ServiceCall;
  constructor(call: ServiceCall) {
    this.call = call;
  }

  public Login(loginAttempt: LoginCredentials): Observable<any> {
    let myResponse = new Map<string, string>()
    let data = new Map<string, string>();
    let data2 = new Map<string, string>();
    let url = "Account/Login";
    data.set('Username', loginAttempt.username);
    data.set('Password', loginAttempt.password);
    return this.call.makeCall(url, 'POST', data).pipe(map(response => data2 = response));
  }
}

serviceCall.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders, HttpRequest, HttpResponse,  } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ServiceCall {
  private _http: HttpClient;
  private _urlAppend: string;
  private _isAuthenticated: boolean;

  constructor(http: HttpClient) {
    this._http = http;
    this._urlAppend = '/api/';
    this._isAuthenticated = false;
  }

  public makeCall(url: string, type: string, data: Map<string, string>): Observable<any> {
    url = this._urlAppend + url;
    let headers = new HttpHeaders();
    headers.append('Content-Type', 'application/json');
    headers.append('charset', 'utf-8');
    let params = new HttpParams();
    let result = new Response();

    data.forEach((value: string, key: string) => {
      params.set(key, value);
    });

    let options = { headers: headers, params: params, withCredentials: this._isAuthenticated };
    let body = JSON.stringify(data);

    if (type == "GET") {
      return this._http
        .get(url, options)
        .pipe(map((result: Response) => result));
    } else if (type == "POST") {
      return this._http
        .post(url, body, options)
        .pipe(map(this.extractData));
    } else {
      Observable.throw("Invalid command.");
    }
  }

  public setAuthentication(input: boolean) {
    this._isAuthenticated = input;
  }

  private extractData(res: Response) {
    let body = res.json();
    return body || {};
  }

  private generateQueryString(input: Map<string, string>) {
    let response = new URLSearchParams();

    input.forEach((value: string, key: string) => {
      response.append(key, value);
    });

    return response;
  }
}

And, finally, the 404 response that shows up in Chrome's console:

HttpErrorResponse
{
    error: null
    headers: HttpHeaders
    {
        lazyInit: f()
        lazyUpdate: null
        normalizeNames: Map(0) {}
    }
    message:  "Http failure response for http://localhost:4200/api/Acocunt/Login: 404 Not Found"
    name:  "HttpErrorResponse"
    ok:  false
    status: 404
    statusText:  "Not Found"
    url:  "http://localhost:4200/api/Account/Login"
}

Solution

  • Ugh. The issues with this one. First, on the C# side: I was used to older Web API with the 'regular' .NET framework and there were some changes when using .NET Core. In the AccountController.cs file, the "branch" for that controller needed to be specified right above the class declaration, to have something like [Route("api/[controller]")] right above the class declaration. Then, on each method within the class, I had to define what that specific method's piece of URL was. In the case of above,

    [HttpPost]
    public LoginResult Login(LoginAttempt input)
    

    needed to look like:

    [Route("login")]
    [HttpPost]
    public IActionResult Login([FromBody]LoginAttempt input)
    

    And then on the Typescript side of things, the Angular2 site was ignoring the proxy information and not trying to forward to my API port. I had to have my proxy information look like the following in my proxy.conf.json file:

    "/api": {
      "target": "http://localhost:5000",
      "secure": false,
      "changeorigin": true,
      "logLevel": "debug",
      "pathRewrite": {"^/api": ""}
    }
    

    I tried every version I could find and it would not work until I added the "changeorigin" bit to it. This allowed things to work as they should for me.