[ionic view] Add Cordova http plugin to handle cors


#1

Hi,

Not sure this is the right place for that, but based on the ionic view plugins supported https://ionicframework.com/docs/pro/view.html#plugin-support there is no plugin to make native http requests to handle cors issue.

Is it possible to add one like https://ionicframework.com/docs/native/http/ ?

Thanks

JB


#2

Hey JB,

Unfortunately CORS is enforced by the new WebKit framework on iOS, so Ionic or Cordova can’t do much about this. CORS needs to be handled on the server-side (i.e. the person who manages the service that your app is talking to needs to allow for your app to talk to it by changing the service’s or server’s config or setup). You can read more on this here: https://enable-cors.org/. Alternatively, you can downgrade the web view component and use UIWebView, which doesn’t enforce CORS, instead of WKWebView. There’s a lot more information here including steps on how you can downgrade to the previous web view component for iOS: https://ionicframework.com/docs/wkwebview/#cors and https://ionicframework.com/docs/wkwebview/#downgrading-to-uiwebview

Let me know if I misunderstood your question!

Cheers,
Smik


#3

Actually you can bypass cors issues with the plugin l mentioned earlier. That’s what I’ve done on some app where I can’t configure the server. But this plugin is not supported by ionic view so you can’t test your app with it.


#4

Hi, can you show how did you bypass cors with the http plugin?


#5
import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular';
import { Observable } from 'rxjs/Observable';
import { HTTP, HTTPResponse } from '@ionic-native/http';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/defer';
import 'rxjs/add/observable/fromPromise';

@Injectable()
export class HttpService {

  private cachedObs = new Map<string, Observable<any>>();

  constructor(private platform: Platform, private cordovaHttp: HTTP) {
  }

  request<T>(method: 'GET' | 'POST' | 'DELETE', url: string, options: {
    body?: any;
    params: Map<any, any>;
    headers?: Headers;
    responseType: 'json' | 'text';
  }): Observable<HttpServiceResponse<T | any>> {
    const urlWithParams = this.getUrlWithParams(url, options.params);
    let obs: Observable<any>;
    let obsKey = this.getObservableCacheKey(method, url, options);


    if (this.cachedObs.has(obsKey)) {
      return this.cachedObs.get(obsKey);
    }

    if (this.platform.is('cordova')) {
      obs = this.cordovaRequest<T>(method, urlWithParams, options);
    } else {
      obs = this.browserRequest<T>(method, urlWithParams, options);
    }



    obs = obs
      .map(data => {
        this.cachedObs.delete(obsKey);
        return data
      })
      .share();

    this.cachedObs.set(obsKey, obs);

    return obs;

  }

  get<T>(url: string, options?: {
    headers?: Headers;
    params?: Map<any, any>;
    responseType?: 'json' | 'text';
    getRawResponse?: boolean;
  }): Observable<HttpServiceResponse<T | any> | T | any> {
    let obs = this.request<T>('GET', url, {
      headers: options ? options.headers : null,
      params: options ? options.params : null,
      responseType: options ? options.responseType : 'json',
    });

    if (options && options.getRawResponse) {
      return obs;
    } else {
      return obs.map(res => this.handleReponse(res));
    }
  }


  post<T>(url: string, body: any, options?: {
    headers?: Headers;
    params?: Map<any, any>;
    responseType?: 'json' | 'text';
    getRawResponse?: boolean;
  }): Observable<HttpServiceResponse<T | any> | T | any> {
    let obs = this.request<T>('POST', url, {
      body: body,
      headers: options ? options.headers : null,
      params: options ? options.params : null,
      responseType: options ? options.responseType : 'json',
    });

    if (options && options.getRawResponse) {
      return obs;
    } else {
      return obs.map(res => this.handleReponse(res));
    }
  }


  delete<T>(url: string, body: any, options?: {
    headers?: Headers;
    params?: Map<any, any>;
    responseType?: 'json' | 'text';
    getRawResponse?: boolean;
  }): Observable<HttpServiceResponse<T | any> | T | any> {
    let obs = this.request<T>('DELETE', url, {
      body: body,
      headers: options ? options.headers : null,
      params: options ? options.params : null,
      responseType: options ? options.responseType : 'json',
    });

    if (options && options.getRawResponse) {
      return obs;
    } else {
      return obs.map(res => this.handleReponse(res));
    }
  }

  private handleReponse<T>(response: HttpServiceResponse<T>): Observable<any> | T {
    if (!response.ok) {
      throw Observable.throw(response);
    }

    return response.data
  }

  private getUrlWithParams(url, params?: Map<any, any>) {
    let queryStrings = [];

    if (params) {
      params.forEach((value, key) => {
        if (Array.isArray(value)) {
          value.forEach(v => {
            queryStrings.push(standardEncoding(key) + '=' + standardEncoding(v));
          })
        } else {
          queryStrings.push(standardEncoding(key) + '=' + standardEncoding(value));
        }
      })
    }
    if (queryStrings.length === 0) {
      return url;
    }
    // Does the URL already have query parameters? Look for '?'.
    const qIdx = url.indexOf('?');
    // There are 3 cases to handle:
    // 1) No existing parameters -> append '?' followed by params.
    // 2) '?' exists and is followed by existing query string ->
    //    append '&' followed by params.
    // 3) '?' exists at the end of the url -> append params directly.
    // This basically amounts to determining the character, if any, with
    // which to join the URL and parameters.
    const sep: string = qIdx === -1 ? '?' : (qIdx < url.length - 1 ? '&' : '');

    return url + sep + queryStrings.join('&');

  }

  private browserRequest<T>(method: 'GET' | 'POST' | 'DELETE', url: string, options: {
    body?: any;
    headers?: Headers;
    responseType: 'json' | 'text';
  }): Observable<HttpServiceResponse<T>> {

    const responseType = options.responseType || 'json';

    return Observable.defer(() => {
      return Observable.fromPromise(
        fetch(url, {
          method: method,
          body: options.body ? JSON.stringify(options.body) : null,
          headers: options.headers || new Headers(),
        })
          .then(
          (response: Response) => {
            const httpResponse: HttpServiceResponse<T> = {
              ok: response.ok,
              status: response.status,
              data: null
            };
            if (!response.ok) {
              httpResponse.errorMessage = 'Error ' + response.status + ' on calling ' + method + ' ' + url;
              return httpResponse;
            }

            const contentType = response.headers.get("content-type");
            if (contentType && contentType.includes("application/json")) {
              return response.json().then(json => {
                httpResponse.data = json;
                return httpResponse;
              });
            } else {
              return response.text().then(text => {
                httpResponse.data = responseType === 'json' ? JSON.parse(text) : text;
                return httpResponse;
              });
            }

          },
          (err: TypeError) => {
            return <HttpServiceResponse<T>>{
              ok: false,
              status: null,
              data: null,
              errorMessage: err.message
            };
          }
          )
      );
    });


  }

  private cordovaRequest<T>(method: 'GET' | 'POST' | 'DELETE', url: string, options: {
    body?: any;
    headers?: Headers;
    responseType: 'json' | 'text';
  }): Observable<HttpServiceResponse<T>> {


    const responseType = options.responseType || 'json';

    let headers = {};

    if (options.headers) {
      options.headers.forEach((value, key) => {
        headers[key] = value;
      })
    }
    this.cordovaHttp.setDataSerializer('json');

    let promise;

    if (method === 'GET') {
      promise = this.cordovaHttp.get(url, {}, headers);
    } else if (method === 'POST') {
      promise = this.cordovaHttp.post(url, options.body, headers);
    } else if (method === 'DELETE') {
      promise = this.cordovaHttp.delete(url, options.body, headers);
    }


    return Observable.defer(() => {
      return Observable.fromPromise(
        promise
          .then(
          (response: HTTPResponse) => {
            const httpResponse: HttpServiceResponse<T> = {
              ok: response.status >= 200 && response.status <= 299,
              status: response.status,
              data: null
            };

            if (httpResponse.ok) {
              httpResponse.data = responseType === 'json' ? JSON.parse(response.data) : response.data;
            } else {
              httpResponse.errorMessage = 'Error ' + response.status + ' on calling ' + method + ' ' + url;
            }

            return httpResponse;
          },
          response => {
            const httpResponse: HttpServiceResponse<T> = {
              ok: response.status >= 200 && response.status <= 299, // Weird isn't it?
              status: response.status,
              data: null
            };

            if (httpResponse.ok) {
              httpResponse.data = responseType === 'json' ? JSON.parse(response.error) : response.error;
            } else {
              httpResponse.errorMessage = response.error;
            }

            return httpResponse;
          }
          )
      )
    });
  }

  private getObservableCacheKey(method, url: string, options: {
    body?: any;
    params: Map<any, any>;
    headers?: Headers;
    responseType: 'json' | 'text';
  }) {

    let headers = {};
    if (options.headers) {
      options.headers.forEach((value, key) => {
        headers[key] = value;
      })
    }

    let params = {};
    if (options.params) {
      options.params.forEach((value, key) => {
        params[key] = value;
      })
    }

    return JSON.stringify({
      method: method,
      url: url,
      body: options.body,
      params: params,
      headers: headers,
      responseType: options.responseType,
    });
  }


}

export interface HttpServiceResponse<T> {
  ok: boolean;
  status: number;
  data: T;
  errorMessage?: string;
}


//Get from angular httpClient
function standardEncoding(v: string): string {
  return encodeURIComponent(v)
    .replace(/%40/gi, '@')
    .replace(/%3A/gi, ':')
    .replace(/%24/gi, '$')
    .replace(/%2C/gi, ',')
    .replace(/%3B/gi, ';')
    .replace(/%2B/gi, '+')
    .replace(/%3D/gi, '=')
    .replace(/%3F/gi, '?')
    .replace(/%2F/gi, '/');
}

Use this service instead of the angular one (Http or HttpClient). It will also make your app smaller.


#6

Thanks for your help.
I have tried the HTTP native but no luck, still got “CDVWKWebViewEngine: trying to inject XHR polyfill” error and the content is not loaded.
Am I doing something wrong?

app.module

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { HTTP } from '@ionic-native/http';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { Network } from '@ionic-native/network';


import { MyApp } from './app.component';
import { ApiProvider } from '../providers/api';
import { Connectivity } from '../providers/connectivity';

@NgModule({
  declarations: [
    MyApp
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp,{
      backButtonText: '',
    }),
    HttpModule
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp
  ],
  providers: [
    StatusBar,
    SplashScreen,
    ApiProvider,
    Network,
    Connectivity,
    HTTP,
    {provide: ErrorHandler, useClass: IonicErrorHandler}    
  ]
})
export class AppModule {}

Provider

import { Injectable } from '@angular/core';
import { HTTP } from '@ionic-native/http';
import 'rxjs/add/operator/map';

constructor(private http: HTTP) {}

getHomeSlides(){

    let headers = {'Content-Type': 'application/json', 'Cache-Control': 'no-cache'};

    return new Promise<any>((resolve) => {
        this.http.get(this.apiURL + this.apiPostsHome, {}, headers)
        .then((response) => {
            this.homeBanner = JSON.parse(response.data);
            resolve(this.homeBanner);
        })
    })
  }

#7

This code should works. You should create a blank project and only install this http plugin and make it works. Once done compare dependencies with your current project.

Btw you the http’s get method already returns a promise, you don’t have to create a new one:

return this.http.get(this.apiURL + this.apiPostsHome, {}, headers)
        .then((response) => {
            this.homeBanner = JSON.parse(response.data);
            return this.homeBanner;
        })

#8

Holly s*** you saved me!
Thanks!


#9

If help to anybody, just use this extension for google chrome, on builded apps you dont need to do anything, its a simple toggle that you can set on/off: https://chrome.google.com/webstore/detail/cors-toggle/jioikioepegflmdnbocfhgmpmopmjkim