Easy NativeScript Network Spy Using a Custom Interceptor
What's a simple thing all developers really want from complex, network-bound mobile app? We want to find network related actions and errors…
Take control of your career. Build JavaScript mobile apps.
Catch Dave Coffin, Nathan Walker, and Alex Ziskind at ngAtlanta in February 2020 for an advanced NativeScript with Angular workshop called Breathe life into mobile UX with solid architecture lessons
. You can register now and take your NativeScript skills up a notch. Register here.
What's a simple thing all developers really want from complex, network-bound mobile app?
We want to find network related actions and errors as quickly as possible, without all the headaches of setting up proxies and certificates to sniff our HTTP requests and responses. We want to very that all the headers are correct, perhaps after login, perhaps even the JWT! Here'a simple solution.
Current State of Affairs
Currently we program against an IDE and a console. Normally if we want to see the network traffic we can attach Fiddler (or Charles if you're on a Mac, or some other sniffer) and we can also start debug mode using Chrome.
Now, this is where things gets complicated.
Working in debug mode just to see network operations can make our life a bit harder. Why? Chrome's socket sometimes disconnect and you need to tns run android --start
or tns debug android
again and again.
What about Fiddler or Wireshark? Well you'd have to create a proxy so that your device will make network operations through your network card and not by its own. Not to mention that if an SSL traffic is occurring, then fiddler has its own certificate which can break trust certificate chain. You'd have to install its certificate in the device trusted certificates.
And I ask - why all this mess? All I've asked is to see request summary, response (all or brief) summary - just to see that the server likes me and that things are going well. That's all.
Intercept This
Let's dive right into the code, and I'll explain along the way. First we need to create a custom interceptor TypeScript file.
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CustomInterceptor implements HttpInterceptor {
log = (req: HttpRequest<any>) =>
tap(
(res: HttpEvent<any>) => {
if (res instanceof HttpResponse) {
console.log(this.logJson(req, res));
}
},
res => {
if (res instanceof HttpErrorResponse) {
console.log(this.logJson(req, res));
}
}
);
logJson(req: HttpRequest<any>, res): string {
return `
Method=${req.method}
Url=${req.urlWithParams}
Url Params=${JSON.stringify(
req.params
.keys()
.reduce((acc, c) => ((acc[c] = req.params.get(c)), acc), {})
)}
Body Params=${JSON.stringify(req.body, null, ' ')}
Headers=${JSON.stringify(
req.headers.keys().map(x => ({ [x]: req.headers.get(x) })),
null,
' '
)}
----------------------------------------------------------------------------
Response=${JSON.stringify(res, null, ' ')}
Headers=${JSON.stringify(
res.headers.keys().map(x => ({ [x]: res.headers.get(x) })),
null,
' '
)}`;
}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(req).pipe(this.log(req));
}
}
The logJson
method will read the request and response headers, response, query parameters and body parameters. We will then display it nicely through JSON.stringify
with the overload of null, " "
The method will show log the request and response like this:
request ...
----------------------------------------
response ...
Before using this interceptor, we must declare it in the app module file in the providers section:
providers: [
...
{
provide : HTTP_INTERCEPTORS,
useClass: CustomInterceptor,
multi : true
}
],
That's all actually. You don't have to do anything else.
Taking It for a Spin
Let's try to create a network request. You can use my free json-server persistent crud tester that I've build
Let's first create a button
<StackLayout class="page">
<button (tap)="onButtonTap($event)"></button>
</StackLayout>
which invokes this method
onButtonTap($event) {
this._dataService.getData().subscribe(
res => {
console.log(res);
},
error => {}
);
}
Where _dataService
is of the type DataService
, and it's just a service to fetch data from. This is the DataService
implementation
@Injectable()
export class DataService {
constructor(private _httpService: HttpService) {}
public getData<T>(params?: { [param: string]: string }): Observable<T> {
return this._httpService.get<T>(
'https://servercrud2.herokuapp.com/posts/1',
params
);
}
}
And HttpService
is a service which is responsible for the actual network operations
@Injectable()
export class HttpService {
constructor(private _http: HttpClient) {}
public get<T>(
url: string,
params: { [param: string]: string }
): Observable<T> {
return this._http.get<T>(url, { params });
}
}
So if we tap the button
We can see this log printed out
Method=GET
Url=https://servercrud2.herokuapp.com/posts/1
Url Params={}
Body Params=null
Headers=[]
----------------------------------------------------------------------------
Response={
"headers": {
"normalizedNames": {},
"lazyUpdate": null
},
"status": 200,
"statusText": "OK",
"url": "https://servercrud2.herokuapp.com/posts/1",
"ok": true,
"type": 4,
"body": {
"id": 1,
"title": "json-server",
"author": "typicode"
}
}
Headers=[
{
"null": "HTTP/1.1 200 OK"
},
{
"Access-Control-Allow-Credentials": "true"
},
{
"Cache-Control": "no-cache"
},
{
"Connection": "keep-alive"
},
{
"Content-Length": "63"
},
{
"Content-Type": "application/json; charset=utf-8"
},
{
"Date": "Sat, 18 Aug 2018 18:51:10 GMT"
},
{
"Etag": "W/\"3f-ApGYAlkiEU7eUDcnHqNL7illO4Y\""
},
{
"Expires": "-1"
},
{
"Pragma": "no-cache"
},
{
"Server": "Cowboy"
},
{
"Vary": "Origin, Accept-Encoding"
},
{
"Via": "1.1 vegur"
},
{
"X-Android-Received-Millis": "1534618269584"
},
{
"...
{
"id": 1,
"title": "json-server",
"author": "typicode"
}
Well, as you can see in the same console, you get to see all what you need. No certificates installations. No Fiddler or Wireshark. No nothing!
You might ask yourself: Well what if I fetch two hundred records of JSON objects? I don't want to see it all in the screen.
Well there is an answer to that too. Do you see this ...
?
Since version 4.1, NativeScript provides the ability to truncate or enlarge the amount of console log. The default is 1024 characters.
You can set it in the package.json
file:
"android": {
"maxLogcatObjectSize": 2048
}
Let's change the maxLogcatObjectSize
value to 512.
Here's the result:
Method=GET
Url=https://servercrud2.herokuapp.com/posts/1
Url Params={}
Body Params=null
Headers=[]
----------------------------------------------------------------------------
Response={
"headers": {
"normalizedNames": {},
"lazyUpdate": null
},
"status": 200,
"statusText": "OK",
"url": "https://servercrud2.herokuapp.com/posts/1",
"ok": true,
"type": 4,
"body": {
"id": 1,
"title": "json-server",
"author": "typicode"
}
}
Headers=[
{
"null": "HTTP/1.1 200 OK"
},
{
"Access-Control-Al...
{
"id": 1,
"title": "json-server",
"author": "typicode"
}
Much shorter, right?
Well you get the point. It helps you see request info and response info.
Errors Anyone?
Do notice the -------------------------------
line. Again, above this line is the request and below it is the response.
Let's try something which will cause an error.
Change the url to
public getData<T>(params?: { [param: string]: string }): Observable<T> {
return this._httpService.get<T>("https://servercrud2.herokuapp.com/posts/notExists", params);
}
Notice notExists
. Now let's try it again.
Method=GET
Url=https://servercrud2.herokuapp.com/posts/notExists
Url Params={}
Body Params=null
Headers=[]
----------------------------------------------------------------------------
Response={
"headers": {
"normalizedNames": {},
"lazyUpdate": null
},
"status": 404,
"statusText": "Not Found",
"url": "https://servercrud2.herokuapp.com/posts/notExists",
"ok": false,
"name": "HttpErrorResponse",
"message": "Http failure response for https://servercrud2.herokuapp.com/posts/notExists: 404 Not Found",
"error": {}
}
Headers=[
{
"null": "HTTP/1.1 404 Not Found"
},
{
"Access-Control-Allow-Credentials": "true"
},
{
"Cache-Control": "no-cache"
},
{
"Connection": "keep-alive"
},
{
"Content-Length": "2"
},
{
"Content-Type": "application/json; charset=utf-8"
},
{
"Date": "Sat, 18 Aug 2018 19:00:43 GMT"
},
{
"Etag": "W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\""
},
{
"Expires": "-1"
},
{
"Pragma": "no-cache"
},
{
"Server": "Cowboy"
},
{
"Vary": "Origin, Accept-Encoding"
...
You can easily see what response you get, what the headers are, etc. You might want to add an AppSettings
injected service where you have:
isEnableHttpLogging:true
So that the interceptors will log only by a conditional environment flag.
Final Words
This technique saved me a lot of effort trying to declare proxies, certificates (to decypher the SSL content), and watch yet another window on my desktop. It is extremely simple to notice and verify that you send the right stuff, and get the right stuff back.
Until the next article, Take care. ;-)