skip to content

With Angular's HttpClient, how do the observe and responseType options change what http.get() emits, for example when you need a response header?

level: middleimportance: should knowfreq 48%

answer

  1. body is only the default
  2. three observe values
  3. four ways to read the payload
  4. literal types pick the overload

basics

~20 s

observe picks what each emission is: the body (default), the full HttpResponse with status and headers, or the HttpEvent stream. responseType picks how the body is read: json (default), text, blob or arraybuffer. Both need literal values to select the typed overload.

solid answer

~40 s

By default `http.get<T>()` uses `observe: 'body'` and `responseType: 'json'`, so it emits the parsed body typed as `T` and nothing else. To read a header or the status, pass `observe: 'response'` and you get an `HttpResponse<T>` with `status`, `headers` and `body`. `observe: 'events'` exposes the request lifecycle as `HttpEvent`s — `Sent`, progress events when requested, and the final `Response` — which is how you show download progress. `responseType` switches the body to a `string` (`'text'`), a `Blob` or an `ArrayBuffer`. Each combination is a separate TypeScript overload, so the values must be literal types: extract the options into a variable and you need `as const`. In Angular 22, `reportProgress` is deprecated in favour of `reportUploadProgress` and `reportDownloadProgress`, and the default `FetchBackend` cannot report upload progress.

code

ts · 43 lines
ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpEventType } from '@angular/common/http';
import { Observable, map } from 'rxjs';

export interface Forecast {
  city: string;
  tempC: number;
}

@Injectable({ providedIn: 'root' })
export class WeatherService {
  private readonly http = inject(HttpClient);

  // observe: 'response' exposes status and headers alongside the typed body.
  getForecastWithIssueTime(city: string): Observable<{ forecast: Forecast | null; issuedAt: string | null }> {
    return this.http
      .get<Forecast>('/api/forecast', { params: { city }, observe: 'response' })
      .pipe(map((res) => ({ forecast: res.body, issuedAt: res.headers.get('X-Forecast-Issued') })));
  }

  // responseType: 'blob' returns Observable<Blob>; no type argument on this overload.
  getRadarImage(region: string): Observable<Blob> {
    return this.http.get(`/api/radar/${region}.png`, { responseType: 'blob' });
  }

  // observe: 'events' with download progress for a large CSV export.
  downloadArchive(onProgress: (loaded: number, total?: number) => void): Observable<string | null> {
    return this.http
      .get('/api/forecast/archive.csv', {
        responseType: 'text',
        observe: 'events',
        reportDownloadProgress: true,
      })
      .pipe(
        map((event) => {
          if (event.type === HttpEventType.DownloadProgress) {
            onProgress(event.loaded, event.total);
          }
          return event.type === HttpEventType.Response ? event.body : null;
        }),
      );
  }
}

go deeper

for a junior

Know that the default emission is just the parsed body, and that observe: 'response' is how you get status and headers.

for a middle

Explain the observe and responseType matrix, why the generic only applies to JSON, and why extracted options need as const to select the right overload.

for a senior

Anticipate the production gotchas: unexposed CORS headers returning null, null bodies on 204, and progress reporting that changed with the Angular 22 FetchBackend default.

for a principal

Set a convention for which layer unwraps HttpResponse and events, so components receive domain values and transport details stay in data services.

## Two independent dials Every `HttpClient` request method takes an options object. Two options decide what the returned Observable emits: - **`observe`** — *what wraps the payload*: just the body, the whole response, or every lifecycle event. - **`responseType`** — *how the payload is read*: parsed JSON, a string, a `Blob` or an `ArrayBuffer`. They combine freely, and each combination has its own overload of `get`, `post`, `put` and the rest, so the static type of the result follows the options you pass. ## `observe`: body, response or events | `observe` value | Emits | Typical use | |---|---|---| | `'body'` (default) | The body once, then completes | Most data fetching | | `'response'` | One `HttpResponse<T>` with `status`, `statusText`, `headers`, `url`, `body` | Reading a header such as a pagination total or an issued-at timestamp | | `'events'` | A stream of `HttpEvent<T>` values, ending with the `Response` event | Progress bars, knowing when the request was sent, custom interceptor events | With `observe: 'response'`, `body` is typed `T | null`, because a response such as a 204 has no body; with `responseType: 'json'` an empty body is parsed to `null`. The `HttpEventType` enum names the events you can meet with `observe: 'events'`: 1. `Sent` — the request was dispatched. 2. `UploadProgress` — bytes of the request body sent (only when requested and supported). 3. `ResponseHeader` — status and headers arrived; only the XHR backend emits it, on the first download-progress event, and the default Fetch backend does not emit it at all. 4. `DownloadProgress` — bytes of the response received; with `responseType: 'text'` it can carry `partialText`. 5. `Response` — the complete `HttpResponse`. 6. `User` — a custom event emitted by an interceptor or backend. Progress events are off by default because each one is an emission your code must handle. Since **Angular 22**, the single `reportProgress` flag is **deprecated** in favour of `reportUploadProgress` and `reportDownloadProgress`. The default `FetchBackend` cannot report upload progress: a request with `reportUploadProgress: true` fails with an error whose development-mode message tells you to configure `withXhr()`. ## `responseType`: how the body is read | `responseType` | Body type | Example | |---|---|---| | `'json'` (default) | The generic `T` (an assertion, not a check) | A forecast object | | `'text'` | `string` | A CSV export | | `'blob'` | `Blob` | A radar image to show with an object URL | | `'arraybuffer'` | `ArrayBuffer` | Raw bytes for a decoder | Only the JSON overloads take a type argument. The `text`, `blob` and `arraybuffer` overloads return fixed types, so `get<string>(url, { responseType: 'text' })` does not compile — drop the type argument. In `observe: 'body'` mode, `HttpClient` also checks at runtime that a `blob`, `text` or `arraybuffer` body really has that type (an interceptor could have replaced it) and throws if not; JSON bodies are not checked. ## The literal-type trap Because overloads are selected by the *literal* values `'response'`, `'events'`, `'text'` and so on, this fails to compile: ```ts const opts = { observe: 'response', responseType: 'text' }; this.http.get('/api/forecast.csv', opts); // opts.observe is widened to string ``` TypeScript widens the properties to `string`, which matches no overload. Keep the object literal inline, or write `observe: 'response' as const` (or `as const` on the whole object) when you extract options into a helper. ## Picking the combination for the job In a weather-lookup service the options map onto needs like this: - **The forecast object for a card**: the defaults, `get<Forecast>(url)`. - **The forecast plus the time it was issued, sent as a header**: `observe: 'response'`, then map to `{ forecast, issuedAt }`. - **A radar image**: `responseType: 'blob'`, then `URL.createObjectURL()` and a cleanup call to `URL.revokeObjectURL()` when the image is replaced. - **A multi-megabyte CSV archive with a progress bar**: `responseType: 'text'`, `observe: 'events'`, `reportDownloadProgress: true`. The wider the option, the more of the transport leaks into your code, so choose the narrowest one that answers the need. ## Reading a header safely - Use `res.headers.get('X-Forecast-Issued')`; lookups are case-insensitive and return `null` when the header is absent. - In the browser, a cross-origin response only exposes custom headers that the server lists in `Access-Control-Expose-Headers`; otherwise `get()` returns `null` even though devtools shows the header. - Map the `HttpResponse` down to what the component needs inside the service, so templates never deal with the wrapper.

  • Why does a custom response header read through Angular's HttpClient come back null even though the browser's devtools show it?
    For a cross-origin request the browser only lets page scripts read safelisted response headers plus the ones the server names in `Access-Control-Expose-Headers`. `HttpClient` can only see what the browser exposes, so `res.headers.get('X-Forecast-Issued')` returns `null`. The fix is on the server: add the header to `Access-Control-Expose-Headers`.
  • An Angular 22 app shows an upload progress bar with HttpClient and it stopped working after an upgrade. What changed?
    Angular 22 made `FetchBackend` the default `HttpBackend`, and the Fetch-based backend cannot report upload progress. Code still using the deprecated `reportProgress` flag just stops receiving upload events, while the new `reportUploadProgress: true` makes the request fail with an error pointing to `withXhr()`. Restoring upload progress means configuring `provideHttpClient(withXhr())`, which switches the app back to the XMLHttpRequest backend.

saying these in an interview costs you the question

  • HttpClient always emits the full HttpResponse, so the body must be read from response.body.
  • Response headers can only be read by writing an interceptor.
  • get<string>(url) makes HttpClient read the body as text.
  • Options extracted into a plain variable select the same overload as an inline literal.
  • The default fetch backend in Angular 22 reports upload progress events like XHR did.