Despite advantages of cordova-plugin-advanced-http over JavaScript http requests like background threading and SSL pinning I personally do not really like using this plugin.
That’s mostly for two reasons: First I like Angular‘s HttpClient for its observable based interface. Also if you decide to switch to native http requests at a later stage, it can be a whole lot of work to refactor an app, because cordova-plugin-advanced-http uses promises. The second reason is debugging on a device or in the simulator. As the requests will be handled natively, you are not able to see and inspect them in Safari’s network tab.
So for a recent app project I was forced to use native http and this time I sat down for a bit to think about, if I could find a satisfying solution for my two major pain points. First I thought about a custom http service using observables with an interface similar to HttpClient. But as HttpClient is very comprehensive it didn‘t seem to be the best idea to re-invent the wheel.
So why not make HttpClient use cordova-plugin-advanced-http if available? 🤔
HttpInterceptors to the rescue!
A HttpInterceptor is a simple way provided by Angular to intercept and modify HttpRequests globally before they are sent to the server respectively intercept and modify the server’s response.
My idea was to create a HttpInterceptor that automatically handles requests with the native http plugin, if the app is running on a native device. (Or more accurate, if the plugin is available, as cordova-plugin-advanced-http supports the platform browser.) If running in the browser (with ionic serve for example), the interceptor should just do nothing and the request will be handled by HttpClient as expected.
So let's start by creating a new interceptor called native-http.interceptor.ts
(A good place to put the interceptor would be the core module.)
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class NativeHttpInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request);
}
}
Next we want to check if the app is running on a device or if the interceptor should just do nothing. I'm doing this by verifying if the platform is cordova
.
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.platform.is(‘cordova’)) { return next.handle(request); }
// some native http magic happening here
}
So, let’s write a method to handle the request natively next. The method will take the original request ( request: HttpRequest<any>
) as the only argument and return Promise<HttpResponse<any>>
.
The final method is coming up next, followed by a step-by-step explanation:
private async handleNativeRequest(request: HttpRequest<any>): Promise<HttpResponse<any>> {
const headerKeys = request.headers.keys();
const headers = {};
headerKeys.forEach((key) => {
headers[key] = request.headers.get(key);
});
try {
await this.platform.ready();
const method = <HttpMethod> request.method.toLowerCase();
const nativeHttpResponse = await this.nativeHttp.sendRequest(request.url, {
method: method,
data: request.body,
headers: headers,
serializer: ‘json’,
});
let body;
try {
body = JSON.parse(nativeHttpResponse.data);
} catch (error) {
body = { response: nativeHttpResponse.data };
}
const response = new HttpResponse({
body: body,
status: nativeHttpResponse.status,
headers: nativeHttpResponse.headers,
url: nativeHttpResponse.url,
});
return Promise.resolve(response);
} catch (error) {
if (!error.status) { return Promise.reject(error); }
const response = new HttpResponse({
body: JSON.parse(error.error),
status: error.status,
headers: error.headers,
url: error.url,
});
return Promise.reject(response);
}
}
- First we need to get the headers of the original request and transform them to an object (key value pair). This format is how cordova-plugin-advanced-http expects headers. You can get the keys of the headers of the original request with
request.headers.keys()
and then just loop through them to populate the headers object for the plugin. - Next it is a good idea to wait until the platform is ready:
await this.platform.ready();
- HttpClient’s HttpMethods are uppercase strings, while cordova-plugin-advanced-http expects lowercase strings (see plugin’s docs) . (I created a type for those methods:
type HttpMethod = ‘get’ | ‘post’ | ‘put’ | ‘patch’ | ‘head’ | ‘delete’ | ‘upload’ | ‘download’;
.) - Now we need to run the native http request and get its response. (see plugin’s docs)
- As the response body of cordova-plugin-advanced-http in my case was a stringified JSON object or empty, I had to handle both cases and either return a parsed JSON body or an empty one.
- Finally we build a HttpClient’s HttpResponse and resolve the promise.
- It’s also always a good idea to handle errors. In this case there could be tow types of errors: a plugin error or an error response from the server. A plugin error does not have a status code (
status
) so it is pretty easy to check for that.
And now my final NativeHttpInterceptor in its entirety:
import { Injectable } from ‘@angular/core’;
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from ‘@angular/common/http’;
import { Observable, from } from ‘rxjs’;
import { Platform } from ‘@ionic/angular’;
import { HTTP } from ‘@ionic-native/http/ngx’;
type HttpMethod = ‘get’ | ‘post’ | ‘put’ | ‘patch’ | ‘head’ | ‘delete’ | ‘upload’ | ‘download’;
@Injectable()
export class NativeHttpInterceptor implements HttpInterceptor {
constructor(
private nativeHttp: HTTP,
private platform: Platform,
) { }
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.platform.is(‘cordova’)) { return next.handle(request); }
return from(this.handleNativeRequest(request));
}
private async handleNativeRequest(request: HttpRequest<any>): Promise<HttpResponse<any>> {
const headerKeys = request.headers.keys();
const header = {};
headerKeys.forEach((key) => {
headers[key] = request.headers.get(key);
});
try {
await this.platform.ready();
const method = <HttpMethod> request.method.toLowerCase();
const nativeHttpResponse = await this.nativeHttp.sendRequest(request.url, {
method: method,
data: request.body,
headers: headers,
serializer: ‘json’,
});
let body;
try {
body = JSON.parse(nativeHttpResponse.data);
} catch (error) {
body = { response: nativeHttpResponse.data };
}
const response = new HttpResponse({
body: body,
status: nativeHttpResponse.status,
headers: nativeHttpResponse.headers,
url: nativeHttpResponse.url,
});
return Promise.resolve(response);
} catch (error) {
if (!error.status) { return Promise.reject(error); }
const response = new HttpResponse({
body: JSON.parse(error.error),
status: error.status,
headers: error.headers,
url: error.url,
});
return Promise.reject(response);
}
}
}
This worked pretty neatly in my project and I am very happy with the result. Using cordova-plugin-advanced-http for me is no longer the big pain as it was before.
And what about the debugging issue?
My other pain point was logging. As cordova-plugin-advanced-http handles the requests natively you are not able to see them in Safari’s network tab (when debugging on an iOS device for example). So to see what’s going on I added some console.logs here and there in the interceptor, that I commented out by default.
const method = <HttpMethod> request.method.toLowerCase();
// console.log(‘— Request url’);
// console.log(request.url)
// console.log(‘— Request body’);
// console.log(request.body);
const nativeHttpResponse = await this.nativeHttp.sendRequest(request.url, {
method: method,
data: request.body,
headers: headers,
serializer: ‘json’,
});
let body;
try {
body = JSON.parse(nativeHttpResponse.data);
} catch (error) {
body = { response: nativeHttpResponse.data };
}
const response = new HttpResponse({
body: body,
status: nativeHttpResponse.status,
headers: nativeHttpResponse.headers,
url: nativeHttpResponse.url,
});
// console.log(‘— Response success’)
// console.log(response);
return Promise.resolve(response);
} catch (error) {
if (!error.status) { return Promise.reject(error); }
// console.log(‘— Response error’)
// console.log(error);
Commenting out those lines might not be the most elegant solution, as well as leaving console functions in a production app isn‘t either. But don‘t worry, I wrote about how to painlessly handle console methods in a production environment in this article. 😎
Top comments (0)