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
This commit is contained in:
parent
19114dc11e
commit
529f0a83cb
|
@ -66,6 +66,7 @@ describe('Http Tests', () => {
|
||||||
await checkLogForMessage('GET "assets/config.json"');
|
await checkLogForMessage('GET "assets/config.json"');
|
||||||
expect(await page.configSpan.getText()).toContain('Heroes API URL is "api/heroes"');
|
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('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 () => {
|
it('can fetch the configuration JSON file with headers', async () => {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<span *ngIf="config">
|
<span *ngIf="config">
|
||||||
<p>Heroes API URL is "{{config.heroesUrl}}"</p>
|
<p>Heroes API URL is "{{config.heroesUrl}}"</p>
|
||||||
<p>Textfile URL is "{{config.textfile}}"</p>
|
<p>Textfile URL is "{{config.textfile}}"</p>
|
||||||
|
<p>Date is "{{config.date.toDateString()}}" ({{getType(config.date)}})</p>
|
||||||
<div *ngIf="headers">
|
<div *ngIf="headers">
|
||||||
Response headers:
|
Response headers:
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// #docregion
|
// #docregion
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Config, ConfigService } from './config.service';
|
import { Config, ConfigService } from './config.service';
|
||||||
import { MessageService } from '../message.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-config',
|
selector: 'app-config',
|
||||||
|
@ -40,7 +39,8 @@ export class ConfigComponent {
|
||||||
// #docregion v1
|
// #docregion v1
|
||||||
.subscribe((data: Config) => this.config = {
|
.subscribe((data: Config) => this.config = {
|
||||||
heroesUrl: data.heroesUrl,
|
heroesUrl: data.heroesUrl,
|
||||||
textfile: data.textfile
|
textfile: data.textfile,
|
||||||
|
date: data.date,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// #enddocregion v1
|
// #enddocregion v1
|
||||||
|
@ -71,5 +71,9 @@ export class ConfigComponent {
|
||||||
makeError() {
|
makeError() {
|
||||||
this.configService.makeIntentionalError().subscribe(null, error => this.error = error );
|
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
|
// #enddocregion
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { catchError, retry } from 'rxjs/operators';
|
||||||
export interface Config {
|
export interface Config {
|
||||||
heroesUrl: string;
|
heroesUrl: string;
|
||||||
textfile: string;
|
textfile: string;
|
||||||
|
date: any;
|
||||||
}
|
}
|
||||||
// #enddocregion config-interface
|
// #enddocregion config-interface
|
||||||
// #docregion proto
|
// #docregion proto
|
||||||
|
|
|
@ -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<any>, 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<any>, 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<any>) {
|
||||||
|
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
|
|
@ -6,6 +6,7 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
// #enddocregion interceptor-providers
|
// #enddocregion interceptor-providers
|
||||||
import { AuthInterceptor } from './auth-interceptor';
|
import { AuthInterceptor } from './auth-interceptor';
|
||||||
import { CachingInterceptor } from './caching-interceptor';
|
import { CachingInterceptor } from './caching-interceptor';
|
||||||
|
import { CustomJsonInterceptor , CustomJsonParser, JsonParser} from './custom-json-interceptor';
|
||||||
import { EnsureHttpsInterceptor } from './ensure-https-interceptor';
|
import { EnsureHttpsInterceptor } from './ensure-https-interceptor';
|
||||||
import { LoggingInterceptor } from './logging-interceptor';
|
import { LoggingInterceptor } from './logging-interceptor';
|
||||||
// #docregion interceptor-providers
|
// #docregion interceptor-providers
|
||||||
|
@ -21,6 +22,10 @@ export const httpInterceptorProviders = [
|
||||||
// #docregion noop-provider
|
// #docregion noop-provider
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
|
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
|
||||||
// #enddocregion noop-provider, interceptor-providers
|
// #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: EnsureHttpsInterceptor, multi: true },
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true },
|
{ provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true },
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"heroesUrl": "api/heroes",
|
"heroesUrl": "api/heroes",
|
||||||
"textfile": "assets/textfile.txt"
|
"textfile": "assets/textfile.txt",
|
||||||
|
"date": "2020-01-29"
|
||||||
}
|
}
|
||||||
|
|
|
@ -741,6 +741,10 @@ To do this, set the cloned request body to `null`.
|
||||||
newReq = req.clone({ body: null }); // clear the body
|
newReq = req.clone({ body: null }); // clear the body
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Http interceptor use-cases
|
||||||
|
|
||||||
|
Below are a number of common uses for interceptors.
|
||||||
|
|
||||||
### Setting default headers
|
### Setting default headers
|
||||||
|
|
||||||
Apps often use an interceptor to set default headers on outgoing requests.
|
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`
|
* Caching behavior; for example, `If-Modified-Since`
|
||||||
* XSRF protection
|
* 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.
|
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.
|
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`.
|
||||||
|
|
||||||
|
<code-example
|
||||||
|
path="http/src/app/http-interceptors/custom-json-interceptor.ts"
|
||||||
|
region="custom-json-interceptor"
|
||||||
|
header="app/http-interceptors/custom-json-interceptor.ts">
|
||||||
|
</code-example>
|
||||||
|
|
||||||
|
You can then implement your own custom `JsonParser`.
|
||||||
|
Here is a custom JsonParser that has a special date reviver.
|
||||||
|
|
||||||
|
<code-example
|
||||||
|
path="http/src/app/http-interceptors/custom-json-interceptor.ts"
|
||||||
|
region="custom-json-parser"
|
||||||
|
header="app/http-interceptors/custom-json-interceptor.ts">
|
||||||
|
</code-example>
|
||||||
|
|
||||||
|
You provide the `CustomParser` along with the `CustomJsonInterceptor`.
|
||||||
|
|
||||||
|
<code-example
|
||||||
|
path="http/src/app/http-interceptors/index.ts"
|
||||||
|
region="custom-json-interceptor"
|
||||||
|
header="app/http-interceptors/index.ts">
|
||||||
|
</code-example>
|
||||||
|
|
||||||
|
|
||||||
|
{@a caching}
|
||||||
|
### Caching requests
|
||||||
|
|
||||||
Interceptors can handle requests by themselves, without forwarding to `next.handle()`.
|
Interceptors can handle requests by themselves, without forwarding to `next.handle()`.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue