Angular 4 image async with bearer headers
Asked Answered
L

4

11

My task is to make async image requests with auth headers. I have image paths like this:

<img src="{{file.src}}"/>

And I need to Add Bearer Token to header for such requests. Page contains many images, so ajax requests are don't fit. Have no idea how to do this.

Lamson answered 4/10, 2017 at 11:24 Comment(2)
Why do you think XHR-s are not suitable for that?Punjab
@JánHalaša - what do you mean? I thought may be in Angular 4 there some default thing for this issue, I mean things to Resful API projectLamson
C
14

Assuming you have implemented an HttpIntercepter to add the header, here's a solution that actually does work (in Angular 4):

import { Pipe, PipeTransform } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Pipe({
  name: 'secure'
})
export class SecurePipe implements PipeTransform {

  constructor(private http: HttpClient) { }

  transform(url: string) {

    return new Observable<string>((observer) => {
      // This is a tiny blank image
      observer.next('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');

      // The next and error callbacks from the observer
      const {next, error} = observer;

      this.http.get(url, {responseType: 'blob'}).subscribe(response => {
        const reader = new FileReader();
        reader.readAsDataURL(response);
        reader.onloadend = function() {
          observer.next(reader.result);
        };
      });

      return {unsubscribe() {  }};
    });
  }
}

You use it like this:

<img [src]="'/api/image/42' | secure | async" />

The previous solutions were pretty drastically flawed. I don't guarantee that this is perfect, but it is actually tested and working for me.

You can't return the observable you get from http.get! I don't know why the previous solutions assume you can. The observable for http.get indicates when the data is retrieved from the server. But, there is another asynchronous process that has to be completed after that: the call to reader.readAsDataURL. So you need to create an Observable that you will call next on after that process completes.

Also, if you don't put something into the image immediately, the browser will still try to load the image using HTTP and you get an error in your JavaScript console. That's the reason for the first observer.next call that puts in an empty, tiny GIF image.

An issue with this approach is that if the image is used more than once it will load each one every time. Even if the browser caches, you do the conversion to base64 every single time. I created a cache that stores the image so that future queries are not needed after the first.

Competency answered 5/3, 2018 at 16:35 Comment(1)
I am facing the same issue. Image being downloaded every time. Mind sharing your solution for avoiding this? ThanksAleph
D
7

Now, there is no way to make an Authorized call just via the tag in html, browsers do not provide an API for this, so you will have to make an XHR request. Here is a workaround: get the image via XHR, convert it to blob, then convert blob to base64 and insert image to the src of the tag. This solution will require two pipes to be clear: one is a custom pipe for making XHR calls and the other is the Angular's built-in pipe async. Here is our custom pipe:

import { Pipe, PipeTransform } from '@angular/core';
import { Http, RequestOptions, Headers, ResponseContentType } from @angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';

@Pipe({name: 'image'})
export class ImagePipe implements PipeTransform {
  constructor(private http: Http) {}

  transform(url: string) {
  const headers = new Headers({'Authorization': 'MY TOKEN', 'Content-Type': 'image/*'}); /* tell that XHR is going to receive an image as response, so it can be then converted to blob, and also provide your token in a way that your server expects */
  return this.http.get(url, new RequestOptions({headers: headers, responseType: ResponseContentType.Blob})) // specify that response should be treated as blob data
  .map(response => response.blob()) // take the blob
  .switchMap(blob => {
  // return new observable which emits a base64 string when blob is converted to base64
      return Observable.create(observer => { 
        const  reader = new FileReader(); 
        reader.readAsDataURL(blob); // convert blob to base64
        reader.onloadend = function() {             
              observer.next(reader.result); // emit the base64 string result
        }
      });
    });
  }
}

And here goes your html:

<img [src]="('https://www.w3schools.com/css/trolltunga.jpg' | image) | async" />

We use our pipe to get an observable of a base64 string, and async to insert the actual emitted string inside the src tag.

If you look inside the Network tab you will see that your Authorization header was provided during the XHR call: enter image description here One thing you need to keep in mind is CORS: your image serving server should be configured in a way that it accepts XHR calls for images from the domain your Angular app is running on, also, you will have to provide absolute urls to the custom pipe, otherwise it will make requests to the Angular app's domain itself.

Disgorge answered 4/10, 2017 at 12:22 Comment(4)
This does not work in Angular 4. It must be using a deprecated version of HttpClient?Competency
It is using Http, not HttpClient, yes, it is deprecated. you need to specify some stuff when using HttpClient.Disgorge
@FanJin I don't think so. Videos are a different format and I don;t think you can just base64 a video. Also, Even if you could, videos are usually downloaded and displayed in chunks, so if you try to download and then convert a video in such a format, you woud end up with horrible user experienceDisgorge
for an example, see blog.strongbrew.io/safe-image-requests-in-angularStore
C
0

If you have already implemented HttpInterceptor for api you can simplify above Pipe code by letting interceptor handle headers. Below is updated version using HttpClient.

@Pipe({
  name: 'image',
})
export class ImagePipe implements PipeTransform {
  constructor(
    private http: HttpClient,
    private config: ConfigProvider
  ) { }

  transform(url: string) {
    return this.http.get(url, {responseType: "blob"})
      .switchMap(blob => {
        // return new observable which emits a base64 string when blob is converted to base64
        return Observable.create(observer => {
          const reader = new FileReader();
          reader.readAsDataURL(blob); // convert blob to base64
          reader.onloadend = function () {
            observer.next(reader.result); // emit the base64 string result
          }
        });
      });
  }
}
`

And here is example interceptor:

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  constructor(private config: ConfigProvider) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

      const authReq = req.clone({
        setHeaders: {
          Authorization: this.config.getAuthorization(),
          'X-App-ClientId': this.config.authentication['X-App-ClientId']
        }
      });
      return next.handle(authReq);
    }
}
Chamaeleon answered 22/1, 2018 at 21:12 Comment(2)
This doesn't work. It must be using a deprecated version of HttpClient, since switchMap does not seem to exist anymore or at least does not compile as it is. And, it doesn't recognize that there are two async processes in a row here: http.get and readAsDataURL.Competency
@Charles, just to let you know that switchMap has become an rxjs operator which has to be used inside the pipe function: learnrxjs.io/operators/transformation/switchmap.htmlCoal
B
0

This solution for Angular 5 and a mix of solutions from Armen Vardanyan and Charles. Armen's solution works for Angular 5, but first tries to load http://localhost/null url. To solve it I included Charles' tiny blank image:

@Pipe({name: 'secure'})
export class SecurePipe implements PipeTransform {
  constructor(private http: Http,
    public g: BaseGroupService) {}

    transform(url: string) {

      return new Observable<string>((observer) => {
        observer.next('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
        const {next, error} = observer;

        const headers = new Headers({'Authorization': 'TOKEN', 'Content-Type': 'image/*'}); 
        this.http.get(url, new RequestOptions({headers: headers, responseType: ResponseContentType.Blob})).subscribe(response => {
          const reader = new FileReader();
          reader.readAsDataURL(response.blob());
          reader.onloadend = function() {
            observer.next(reader.result);
          };
        });
        return {unsubscribe() {  }};
      });
    }
}
Barograph answered 11/7, 2018 at 9:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.