Skip to content
Kumar Chandrachooda
Microservices

The Angular App That Forgot Its Auth Header

DShop.Web is a well-structured Angular 6 SPA whose plumbing leaks at every joint - an auth header discarded, PUT and DELETE that both GET, a paged result that starts empty. A tour of bugs that only survive because nobody ran the code.

By Kumar Chandrachooda 07 Oct 2025 5 min read
An HTTP request card with its Authorization line crossed out

The Angular frontend is the estate's clearest demonstration that clean structure and working code are different achievements. DShop.Web is architecturally tidy — feature modules, a shared API base class, typed services — and almost every path through it is broken in a way that a single manual test would have caught. It was written in May 2018, committed for about three weeks, and frozen. Nobody ever clicked through it in anger, and the bugs it left are a precise fossil record of which paths were never run. This part walks them, because each one teaches a specific, transferable Angular gotcha.

The auth header that never leaves

Start with the defect the title promises, in the shared BaseApiService:

private request<TData>(method: string, url: string, data?, params?, isProtected = false) {
  if (isProtected) {
    const token = this.authService.getAccessToken();
    httpOptions.headers.append('Authorization', token);
  }
  httpOptions.body = data;
  httpOptions.params = params;
  return this.http.request<HttpResponse<TData>>(method, `${this.host}/${url}`, httpOptions);
}

Angular's HttpHeaders is immutable. append does not modify the headers in place — it returns a new HttpHeaders instance with the extra entry, and leaves the original untouched. Here the return value is discarded, so httpOptions.headers never gains the Authorization header, and every protected request goes out anonymous. The fix is one character of assignment — httpOptions.headers = httpOptions.headers.append(...) — but the code as written silently authenticates nothing. Against a gateway whose entire security posture is JWT at the front door, an SPA that never sends its token can read only the public routes; the moment it needs the user's cart or orders, it is unauthenticated and doesn't know it.

There is a second bug stacked on the first. httpOptions is a module-level const — one shared object mutated on every request (httpOptions.body = data; httpOptions.params = params). Two overlapping requests race on the same options object; the second clobbers the first's body and params before either completes. A shared mutable request config is a data race waiting for a second concurrent call — which, in a three-week app nobody stress-tested, never arrived.

PUT and DELETE both mean GET

Scroll up in the same file and the verb methods copy-paste themselves wrong:

protected put<TData>(url, data, isProtected = false): Observable<TData> {
  return this.request<TData>('GET', url, data, null, isProtected)   // GET
    .pipe(map(response => response.body));
}

protected delete<TData>(url, isProtected = false): Observable<TData> {
  return this.request<TData>('GET', url, null, null, isProtected)   // GET
    .pipe(map(response => response.body));
}

put issues a GET. delete issues a GET. Both were copied from the get method and never had their verb changed. In most apps this is a five-alarm bug; here it is invisible, because DShop.Web has no screen that edits or deletes anything — it lists products and signs in. The methods exist for completeness, were never called, and so their wrongness never surfaced. Dead code doesn't announce its bugs.

A paged result born empty

The PagedResult class computes its derived fields in field initialisers:

export class PagedResult<TData> {
  totalResults: number = 0;
  pageSize: number = 5;
  pages: number = Math.ceil(this.totalResults / this.pageSize);   // uses 0
  isEmpty: boolean = this.totalResults === 0;                     // true
  items: TData[];

  constructor(totalResults: number, items: TData[]) {
    this.totalResults = totalResults;   // too late
    this.items = items;
  }
}

Field initialisers run before the constructor body. So pages is computed as ceil(0 / 5) = 0 and isEmpty as 0 === 0 = true, using the initial totalResults = 0 — and then the constructor overwrites totalResults with the real value, but pages and isEmpty are already frozen at their empty-state values. Every paged result reports pages: 0, isEmpty: true no matter how many items it holds. The lesson is a TypeScript one people relearn constantly: derived fields belong in getters or the constructor body, never in initialisers that capture a value the constructor is about to change. And pageSize: 5 is hardcoded, so the paging that doesn't compute also isn't configurable.

The typo the template trusts

The product model has a misspelling baked into the property name:

export class ProductModel {
  id: string;
  name: string;
  descirption: string;   // sic
  vendor: string;
  price: number;
}

descirption — and the template binds {{ product.descirption }} to match, so it is internally consistent and runs. But the gateway returns description (spelled correctly), so product.descirption is always undefined and every product card renders a blank description. The typo is doubled — model and template agree with each other and disagree with the server — which is exactly why it survived: nothing in the frontend is inconsistent, so nothing flags it. Only a value arriving from outside reveals that the whole app is reading a field that doesn't exist.

The import from Angular's basement

One import in BaseApiService reaches somewhere it should never go:

import { TData } from '@angular/core/src/render3/interfaces/view';

TData was meant to be a local generic type parameter. Instead the editor auto-imported a same-named symbol from @angular/core/src/render3/... — the private internals of Angular's Ivy renderer, in 2018, before Ivy shipped. It is a deep import into unpublished framework guts for a name that should have been three characters of generic syntax. It happens to compile; it is a dependency on an internal path that could vanish in any Angular patch. The lesson: an auto-import that resolves is not an auto-import that's correct — a src/ path in an import from a framework package is always a mistake.

Logged in until proven otherwise

The AuthService opens with the login state pointing the wrong way:

isUserLogged$: Subject<boolean> = new BehaviorSubject<boolean>(true);

constructor() {
  this.isSessionStored = !!window.sessionStorage.getItem(accessTokenKey);
  this.isUserLogged$.next(!!this.storage.getItem(accessTokenKey));
}

The stream is seeded with true — so for the first synchronous tick, before the constructor runs, every subscriber believes the user is logged in. The constructor then corrects it to the real value, but any component that read isUserLogged$ in that opening instant renders its logged-in view to an anonymous visitor. The safe default for an auth flag is false; you upgrade to true when you have proof, never the reverse. To the author's credit, the storage choice right below it is done properly: isSessionStored picks sessionStorage or localStorage based on a remember-me flag, so “remember me” genuinely survives a browser restart and the un-remembered session genuinely doesn't. One line assumes the best about the user's state; the next handles their preference exactly right.

The one thing done right

To be fair, and to prove the author was capable, the price filter is genuinely good:

this.query$
  .pipe(
    debounceTime(400),
    distinctUntilChanged((prev, curr) =>
      prev.priceFrom === curr.priceFrom && prev.priceTo === curr.priceTo)
  )
  .subscribe(query => this.browse(query));

A Subject debounced by 400ms, with a custom distinctUntilChanged that compares the price bounds by value — and the query is cloned per emission (createFromExisting) so the value comparison isn't defeated by a shared reference. This is rxjs written by someone who understood that distinctUntilChanged defaults to reference equality and worked around it deliberately. The same file that discards its auth header also contains the estate's most fluent piece of reactive code.

The rule of thumb this app leaves you with: structure is not correctness, and an unrun path is an unknown path. DShop.Web passes every architectural review — modules, services, typed models, idiomatic streams — and fails the only test that matters, which is running it. The auth header, the wrong verbs, the empty pages, the phantom field: none is subtle, and all of them survived because the code compiled, looked right, and was never executed. A frontend that is never clicked is a frontend whose bugs are all still in it.

Next, the opposite specimen — naive where this one is tidy, and working where this one is broken: the Blazor app that signs you up behind your back.