From 529f0a83cba1d68d351b3c46a4ddfbaae3ced154 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sat, 30 Jan 2021 22:17:27 +0000 Subject: [PATCH] docs(http): add custom JSONParser example (#40645) Update the HTTP guide and associated example to demonstrate how an interceptor can be used to provide a custom JSON parser. Resolves #21079 PR Close #40645 --- .../examples/http/e2e/src/app.e2e-spec.ts | 1 + .../http/src/app/config/config.component.html | 1 + .../http/src/app/config/config.component.ts | 8 ++- .../http/src/app/config/config.service.ts | 1 + .../custom-json-interceptor.ts | 62 +++++++++++++++++++ .../http/src/app/http-interceptors/index.ts | 5 ++ .../examples/http/src/assets/config.json | 3 +- aio/content/guide/http.md | 43 ++++++++++++- 8 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 aio/content/examples/http/src/app/http-interceptors/custom-json-interceptor.ts diff --git a/aio/content/examples/http/e2e/src/app.e2e-spec.ts b/aio/content/examples/http/e2e/src/app.e2e-spec.ts index 41a4c2e3c7..6a5e78e24d 100644 --- a/aio/content/examples/http/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/http/e2e/src/app.e2e-spec.ts @@ -66,6 +66,7 @@ describe('Http Tests', () => { await checkLogForMessage('GET "assets/config.json"'); expect(await page.configSpan.getText()).toContain('Heroes API URL is "api/heroes"'); expect(await page.configSpan.getText()).toContain('Textfile URL is "assets/textfile.txt"'); + expect(await page.configSpan.getText()).toContain('Date is "Wed Jan 29 2020" (date)'); }); it('can fetch the configuration JSON file with headers', async () => { diff --git a/aio/content/examples/http/src/app/config/config.component.html b/aio/content/examples/http/src/app/config/config.component.html index e17e9bc109..8c2416e39e 100644 --- a/aio/content/examples/http/src/app/config/config.component.html +++ b/aio/content/examples/http/src/app/config/config.component.html @@ -7,6 +7,7 @@

Heroes API URL is "{{config.heroesUrl}}"

Textfile URL is "{{config.textfile}}"

+

Date is "{{config.date.toDateString()}}" ({{getType(config.date)}})

Response headers:
    diff --git a/aio/content/examples/http/src/app/config/config.component.ts b/aio/content/examples/http/src/app/config/config.component.ts index 5afe2d8f8b..c78efe8490 100644 --- a/aio/content/examples/http/src/app/config/config.component.ts +++ b/aio/content/examples/http/src/app/config/config.component.ts @@ -2,7 +2,6 @@ // #docregion import { Component } from '@angular/core'; import { Config, ConfigService } from './config.service'; -import { MessageService } from '../message.service'; @Component({ selector: 'app-config', @@ -40,7 +39,8 @@ export class ConfigComponent { // #docregion v1 .subscribe((data: Config) => this.config = { heroesUrl: data.heroesUrl, - textfile: data.textfile + textfile: data.textfile, + date: data.date, }); } // #enddocregion v1 @@ -71,5 +71,9 @@ export class ConfigComponent { makeError() { this.configService.makeIntentionalError().subscribe(null, error => this.error = error ); } + + getType(val: any): string { + return val instanceof Date ? 'date' : Array.isArray(val) ? 'array' : typeof val; + } } // #enddocregion diff --git a/aio/content/examples/http/src/app/config/config.service.ts b/aio/content/examples/http/src/app/config/config.service.ts index e41c24f39c..7d81ef7281 100644 --- a/aio/content/examples/http/src/app/config/config.service.ts +++ b/aio/content/examples/http/src/app/config/config.service.ts @@ -14,6 +14,7 @@ import { catchError, retry } from 'rxjs/operators'; export interface Config { heroesUrl: string; textfile: string; + date: any; } // #enddocregion config-interface // #docregion proto diff --git a/aio/content/examples/http/src/app/http-interceptors/custom-json-interceptor.ts b/aio/content/examples/http/src/app/http-interceptors/custom-json-interceptor.ts new file mode 100644 index 0000000000..42e63b25c8 --- /dev/null +++ b/aio/content/examples/http/src/app/http-interceptors/custom-json-interceptor.ts @@ -0,0 +1,62 @@ +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; + +// #docregion custom-json-interceptor +@Injectable() +export class CustomJsonInterceptor implements HttpInterceptor { + constructor(private jsonParser: JsonParser) {} + + intercept(httpRequest: HttpRequest, next: HttpHandler) { + if (httpRequest.responseType === 'json') { + // If the expected response type is JSON then handle it here. + return this.handleJsonResponse(httpRequest, next); + } else { + return next.handle(httpRequest); + } + } + + private handleJsonResponse(httpRequest: HttpRequest, next: HttpHandler) { + // Override the responseType to disable the default JSON parsing. + httpRequest = httpRequest.clone({responseType: 'text'}); + // Handle the response using the custom parser. + return next.handle(httpRequest).pipe(map(event => this.parseJsonResponse(event))); + } + + private parseJsonResponse(event: HttpEvent) { + if (event instanceof HttpResponse && typeof event.body === 'string') { + return event.clone({body: this.jsonParser.parse(event.body)}); + } else { + return event; + } + } +} + +// The JsonParser class acts as a base class for custom parsers and as the DI token. +@Injectable() +export abstract class JsonParser { + abstract parse(text: string): any; +} +// #enddocregion custom-json-interceptor + +// #docregion custom-json-parser +@Injectable() +export class CustomJsonParser implements JsonParser { + parse(text: string): any { + return JSON.parse(text, dateReviver); + } +} + +function dateReviver(key: string, value: any) { + // #enddocregion custom-json-parser + if (typeof value !== 'string') { + return value; + } + const match = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(value); + if (!match) { + return value; + } + return new Date(+match[1], +match[2] - 1, +match[3]); + // #docregion custom-json-parser +} +// #enddocregion custom-json-parser diff --git a/aio/content/examples/http/src/app/http-interceptors/index.ts b/aio/content/examples/http/src/app/http-interceptors/index.ts index a44f18f5f9..6a1a7450bc 100644 --- a/aio/content/examples/http/src/app/http-interceptors/index.ts +++ b/aio/content/examples/http/src/app/http-interceptors/index.ts @@ -6,6 +6,7 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; // #enddocregion interceptor-providers import { AuthInterceptor } from './auth-interceptor'; import { CachingInterceptor } from './caching-interceptor'; +import { CustomJsonInterceptor , CustomJsonParser, JsonParser} from './custom-json-interceptor'; import { EnsureHttpsInterceptor } from './ensure-https-interceptor'; import { LoggingInterceptor } from './logging-interceptor'; // #docregion interceptor-providers @@ -21,6 +22,10 @@ export const httpInterceptorProviders = [ // #docregion noop-provider { provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true }, // #enddocregion noop-provider, interceptor-providers + // #docregion custom-json-interceptor + { provide: HTTP_INTERCEPTORS, useClass: CustomJsonInterceptor, multi: true }, + { provide: JsonParser, useClass: CustomJsonParser }, + // #enddocregion custom-json-interceptor { provide: HTTP_INTERCEPTORS, useClass: EnsureHttpsInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true }, diff --git a/aio/content/examples/http/src/assets/config.json b/aio/content/examples/http/src/assets/config.json index a6f2505140..b5f75452ce 100644 --- a/aio/content/examples/http/src/assets/config.json +++ b/aio/content/examples/http/src/assets/config.json @@ -1,4 +1,5 @@ { "heroesUrl": "api/heroes", - "textfile": "assets/textfile.txt" + "textfile": "assets/textfile.txt", + "date": "2020-01-29" } diff --git a/aio/content/guide/http.md b/aio/content/guide/http.md index f401e48d5f..bced5a64b6 100644 --- a/aio/content/guide/http.md +++ b/aio/content/guide/http.md @@ -741,6 +741,10 @@ To do this, set the cloned request body to `null`. newReq = req.clone({ body: null }); // clear the body ``` +## Http interceptor use-cases + +Below are a number of common uses for interceptors. + ### Setting default headers Apps often use an interceptor to set default headers on outgoing requests. @@ -768,7 +772,7 @@ An interceptor that alters headers can be used for a number of different operati * Caching behavior; for example, `If-Modified-Since` * XSRF protection -### Using interceptors for logging +### Logging request and response pairs Because interceptors can process the request and response _together_, they can perform tasks such as timing and logging an entire HTTP operation. @@ -788,9 +792,42 @@ and reports the outcome to the `MessageService`. Neither `tap` nor `finalize` touch the values of the observable stream returned to the caller. -{@a caching} +{@a custom-json-parser} -### Using interceptors for caching +### Custom JSON parsing + +Interceptors can be used to replace the built-in JSON parsing with a custom implementation. + +The `CustomJsonInterceptor` in the following example demonstrates how to achieve this. +If the intercepted request expects a `'json'` response, the `reponseType` is changed to `'text'` +to disable the built-in JSON parsing. Then the response is parsed via the injected `JsonParser`. + + + + +You can then implement your own custom `JsonParser`. +Here is a custom JsonParser that has a special date reviver. + + + + +You provide the `CustomParser` along with the `CustomJsonInterceptor`. + + + + + +{@a caching} +### Caching requests Interceptors can handle requests by themselves, without forwarding to `next.handle()`.