Repository: angular
Issue: Ability to configure custom JSON parser in HttpClient
Labels: feature,help wanted,area: common/http,good first issue,P4,feature: insufficient votes

### Which @angular/* package(s) are relevant/related to the feature request?

common

### Description

There is a common use case to customize JSON parsing in `HttpClient`, which was already discussed in issue https://github.com/angular/angular/issues/21079 (closed without solution). There was also a very simple pull request https://github.com/angular/angular/pull/25027 favoring a `JSON_PARSER` injection token for customizing the parser. This was rejected in https://github.com/angular/angular/pull/25027#issuecomment-769880911 as it was considered fairly straightforward to have the user implement a `JsonHttpInterceptor` as suggested in https://stackblitz.com/edit/angular-ivy-nrdj5q?file=src%2Fapp%2Fcustom-json-parser.ts,src%2Fapp%2Fjson-interceptor.ts.

However, if it were really straightforward, the suggested code would not change the behavior if customized with the original `JSON.parse` method - but it does in the following way:

- It does not strip away the `XSSI_PREFIX` from the response body.
- It does not return `null` for an empty response body.
- It changes the error handling: In case of a parse error, a `SyntaxError` from the `JSON.parse` call is raised on the observable error channel instead of an `HttpErrorResponse` object with its `error` property set to a `HttpJsonParseError` object `{ error, text }` where `error` is the `SyntaxError` and `text` is the original response body that could not be parsed.

This means, errors can no longer be handled as described in https://angular.io/guide/http#handling-request-errors. The user may end up in changing the error handling in unrelated HTTP interceptors and for `httpClient` calls throughout his code.

### Proposed solution

Accepting the pull request https://github.com/angular/angular/pull/25027 would not cause such problems.

### Alternatives considered

Modifying the `JsonHttpInterceptor` from https://stackblitz.com/edit/angular-ivy-nrdj5q?file=src%2Fapp%2Fapp.module.ts to a more adequate solution. The user would need to look up the implementation of `HttpXhrBackend` (https://github.com/angular/angular/blob/main/packages/common/http/src/xhr.ts#L150) and reimplement the following code in a different way in `JsonHttpInterceptor` (which is error-prone rather than straightforward):

```
        // ok determines whether the response will be transmitted on the event or
        // error channel. Unsuccessful status codes (not 2xx) will always be errors,
        // but a successful status code can still result in an error if the user
        // asked for JSON data and the body cannot be parsed as such.
        let ok = status >= 200 && status < 300;

        // Check whether the body needs to be parsed as JSON (in many cases the browser
        // will have done that already).
        if (req.responseType === 'json' && typeof body === 'string') {
          // Save the original body, before attempting XSSI prefix stripping.
          const originalBody = body;
          body = body.replace(XSSI_PREFIX, '');
          try {
            // Attempt the parse. If it fails, a parse error should be delivered to the user.
            body = body !== '' ? JSON.parse(body) : null;
          } catch (error) {
            // Since the JSON.parse failed, it's reasonable to assume this might not have been a
            // JSON response. Restore the original body (including any XSSI prefix) to deliver
            // a better error response.
            body = originalBody;

            // If this was an error request to begin with, leave it as a string, it probably
            // just isn't JSON. Otherwise, deliver the parsing error to the user.
            if (ok) {
              // Even though the response status was 2xx, this is still an error.
              ok = false;
              // The parse error contains the text of the body that failed to parse.
              body = {error, text: body} as HttpJsonParseError;
            }
          }
        }

        if (ok) {
          // A successful response is delivered on the event stream.
          observer.next(new HttpResponse({
            body,
            headers,
            status,
            statusText,
            url: url || undefined,
          }));
          // The full body has been received and delivered, no further events
          // are possible. This request is complete.
          observer.complete();
        } else {
          // An unsuccessful request is delivered on the error channel.
          observer.error(new HttpErrorResponse({
            // The error in this case is the response body (error from the server).
            error: body,
            headers,
            status,
            statusText,
            url: url || undefined,
          }));
        }
      };
```