With Angular's HttpClient, how do the observe and responseType options change what http.get() emits, for example when you need a response header?
answer
- body is only the default
- three observe values
- four ways to read the payload
- literal types pick the overload
basics
~20 sobserve 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 sBy 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 linesimport { 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
Know that the default emission is just the parsed body, and that observe: 'response' is how you get status and headers.
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.
Anticipate the production gotchas: unexposed CORS headers returning null, null bodies on 204, and progress reporting that changed with the Angular 22 FetchBackend default.
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.