[Fixed] In Angular 2, how do you intercept and parse Infinity / NaN bare-words in JSON responses?

Issue

I am writing an Angular front end for an API that occasionally serves Infinity and -Infinity (as bare words) in the JSON response object. This is of course not compliant with the spec, but is handled a few different JSON libraries, albeit optionally. I have an Angular service in place that can successfully retrieve and handle any retrieved entity that does not have these non-conforming values. Additionally, I have managed to get an HttpInterceptor in place which just logs when events trickle through, just to be sure I have it connected properly.

The issue that I am facing is that the HttpInterceptor seems to allow me to do one of two things:

  • Catch/mutate the request before it is sent to the API, or
  • Catch/mutate the request after it comes back from the API, and also after it is parsed.

What I would like to do is very similar to this question for native javascript, but I have not been able to determine if it is possible to tie into the replacer function of JSON.parse in the Angular Observable pipe (I think that if tying into that is possible it would solve my issue).

I have also found this question for Angular which is close, but they appear to have been able to handle changing the response to something other than the bare-words, which I don’t have the liberty of doing.

This is the current implementation of my HttpInterceptor, note that it does not actually make any changes to the body. When retrieving an entity without these bare-word values, it logs to the console and all is well. When retrieving an entity with any of these bare-word values, an error is thrown before the HERE line is hit.

function replaceInfinity(body: string): string {
    // Do something
    return body;
}

@Injectable()
export class JsonInfinityTranslator implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(
            map((event) => {
                if (event instanceof HttpResponse) {
                    console.log("HERE");
                    return event.clone({body: replaceInfinity(event.body)});
                } else {
                    return event;
                }
            })
        );
    }
}

TL;DR: Is there a way to mutate the body text of the returned response before the Angular built in JSON deserialization?

Solution

I was able to figure out how to achieve this, and it came down to:

  1. Modifying the request to return as text instead of json
  2. Catch the text response and replace the bare word symbols with specific string flags
  3. Parse the text into an object using JSON.parse, providing a reviver function to replace the specific string flags with the javascript version of +/-Infinity and NaN

Here’s the Angular HttpInterceptor I came up with:

import {Injectable} from '@angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';

import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

@Injectable()
export class JsonBareWordNumericSymbolTranslator implements HttpInterceptor {
    private static infinityFlag = '__INFINITY_FLAG__';
    private static negInfinityFlag = '__NEG_INFINITY_FLAG__';
    private static nanFlag = '__NAN_FLAG__';

    private static replaceBareWordSymbolsWithFlags(body: string): string {
        const infinityBareWordPattern = /(": )Infinity(,?)/;
        const negInfinityBareWordPattern  = /(": )-Infinity(,?)/;
        const nanBareWordPattern  = /(": )NaN(,?)/;
        return body
            .replace(infinityBareWordPattern, `$1"${this.infinityFlag}"$2`)
            .replace(negInfinityBareWordPattern, `$1"${this.negInfinityFlag}"$2`)
            .replace(nanBareWordPattern, `$1"${this.nanFlag}"$2`);
    }

    private static translateJsonWithFlags(substitutedBody: string): any {
        return JSON.parse(substitutedBody, (key: string, value: string) => {
            if (value === this.infinityFlag) {
                return Infinity;
            } else if (value === this.negInfinityFlag) {
                return -Infinity;
            } else if (value === this.nanFlag) {
                return NaN;
            } else {
                return value;
            }
        });
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (req.responseType !== 'json') {
            // Do not modify requests with response types other than json
            return next.handle(req);
        }

        return next.handle(req.clone({responseType: 'text'})).pipe(
            map((event) => {
                if (!(event instanceof HttpResponse)) {
                    return event;
                }

                const substitutedBody = JsonBareWordNumericSymbolTranslator.replaceBareWordSymbolsWithFlags(event.body);
                const parsedJson = JsonBareWordNumericSymbolTranslator.translateJsonWithFlags(substitutedBody);
                return event.clone({body: parsedJson});
            })
        );
    }
}

Leave a Reply

(*) Required, Your email will not be published