Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Angular HttpClient Streams and Stale Detail Requests

Last updated: 5 Oct 20268 min read
tutorial
IntermediateBy AITrove Editorial

Angular HttpClient returns an observable describing a request. A subscription starts the request; separate subscriptions to a cold HTTP observable can cause separate backend calls. When selected case IDs form a stream, a newer selection should replace the previous detail read. A switching operator can unsubscribe the prior inner request and subscribe to the current one. That is useful for read-only detail loading, not a general retry or cancellation rule for writes. Unsubscribing can stop client work, yet an upstream server may already have processed a request. Mutation idempotency and authorization still belong to server-owned contracts. The UI must also distinguish pending, missing, forbidden, transport failure, and stale response.

Working case

A permit detail panel opens case 47, then case 81 while the first request is slow. Two independent subscriptions race; case 47 returns last and overwrites the case 81 panel. A global interceptor retries every failed request, including approval POSTs; after an approval commits but its response is lost, a retry creates another notification. The revised detail loader switches reads by selected ID and emits a pending state for the active selection. The approval path uses a stable operation ID and a server-owned deduplication result. The interceptor applies only rules that are safe for its route class, not a blanket mutation replay. The server checks case-level permission on every detail and approval call.

Implementation boundary

typescript
import { Injectable, inject } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, of } from "rxjs";
import { catchError, map, startWith, switchMap } from "rxjs/operators";

type PermitDetail = { id: number; title: string };
type DetailState =
  | { kind: "idle" }
  | { kind: "loading"; caseId: number }
  | { kind: "ready"; caseId: number; detail: PermitDetail }
  | { kind: "error"; caseId: number; status: number };

@Injectable()
export class PermitDetailReader {
  private readonly http = inject(HttpClient);
  forSelection(selectedCaseId$: Observable<number | null>): Observable<DetailState> {
    return selectedCaseId$.pipe(switchMap(caseId => {
      if (caseId === null) return of({ kind: "idle" } as DetailState);
      return this.http.get<PermitDetail>(`/api/permits/${caseId}`).pipe(
        map(detail => {
          if (!detail || detail.id !== caseId || typeof detail.title !== "string") {
            throw new Error("Invalid permit detail");
          }
          return { kind: "ready", caseId, detail } as DetailState;
        }),
        catchError(error => of({
          kind: "error", caseId,
          status: error instanceof HttpErrorResponse ? error.status : 0
        } as DetailState)),
        startWith({ kind: "loading", caseId } as DetailState)
      );
    }));
  }
}

Create one selected-ID stream and map it to a typed detail request. Use a switching operator so selection change unsubscribes the previous read; handle null selection without a network call. Put pending, success, and error into a state tagged with the selected ID, so old content is never shown beneath a new heading. Validate the response ID because a TypeScript generic on HttpClient is a compile-time promise, not a runtime parser. Treat 403 as permission failure, 404 as absence when appropriate, and network failure as a retryable transport condition only if the user action allows it. Share a subscribed result deliberately if several UI consumers need it, rather than accidentally issuing duplicate HTTP requests. Keep approval POSTs outside generic read retry logic. Give each mutation an operation identity and let the server return the previous result on replay. Test sign-out, route change, rapid selection, and lost responses.

Cost and boundaries

A switching read can save browser work, but rapid selections may still start several requests before cancellation reaches the network. Sharing one result reduces duplicate calls but needs explicit cache lifetime and account scope. State tagging adds a small object per transition and prevents private content confusion. Validation costs work proportional to payload size; it is essential when trust crosses the network. An interceptor may centralize deadlines and authentication yet can hide retries and amplify an incident if it treats writes like reads. Measure backend request count, canceled bytes, stale publish attempts, and the full user task. A low client error rate cannot prove exactly-once mutation effects.

Failure trace

Select 47 then 81, return 81 first, and make a mock transport ignore unsubscribe for 47; the view still must not publish case 47 beneath case 81. Subscribe to the same HttpClient observable twice and confirm two backend calls unless sharing was deliberate. Return a 403, a 404, malformed JSON with a wrong case ID, and a transport error; each must map to the correct state. Commit an approval, drop its response, and verify a broad interceptor does not blindly replay it as a new write. Sign out during loading and ensure the old private response cannot enter another account's view.

Verification

  • Only the currently selected case can appear in detail state.
  • Separate subscriptions do not accidentally duplicate backend reads.
  • Approval mutations are not retried by a generic read policy.

Practice drill

Build a detail stream for IDs 47, 62, and 81. Use delays of 620, 81, and 47 milliseconds and select the IDs quickly. Record request start, unsubscribe, response, and visible case ID. Add a second subscriber and count network calls before deciding where a shared observable belongs. Give the approval POST a stable operation ID and simulate a lost response after commit. Compare the server audit events before and after a deliberate retry. Write an error table for 403, 404, invalid payload, abort, and offline transport.

Decision note

Switch read requests with selection; classify write retries through an explicit server idempotency contract.

Common Mistakes

  • Treating an HttpClient type argument as runtime validation.
  • Using a broad interceptor to retry every failed POST.
  • Leaving old private detail under a new selection while loading.

Related lessons

Angular Signals and Delivery Boundaries; Angular Signal Sources, Computed Views, and Effect Scope; Angular Component Input, Output, and Row Identity; Angular SSR Hydration and Private Transfer State; Request Deadlines, Retries, and Backoff; Vue Watch Cleanup and Stale Request Ownership.

Apply and check

Build Project: Angular permit operations queue and review Web Development: Angular signals and delivery quiz.

web-tech
web-development
Storage details