3

I'm calling a REST service from an Angular 9 app using HttpClient.get() and I'm not seeing a full list of the response headers. I know that the service is sending them because:

  1. I can see them in the browser debugger Network=>Headers (see image) browser debugger image

and

  1. when I hit the same REST service with a Java app it returns the full headers, about a dozen all together:

java.net.http.HttpHeaders@1627d314 { {access-control-allow-origin=[*], age=[0], connection=[keep-alive], content-length=[1207], content-type=[application/json], date=[Tue, 07 Jul 2020 05:11:45 GMT] <...etc>

What I get from the Angular HttpClient.get() is only one item in the header.keys():

headers: {
  "normalizedNames": {},
  "lazyUpdate": null,
  "lazyInit": null,
  "headers": {}
}

headerKeys:
[
  "content-type: application/json"
]

I created a small sample app to demonstrate the problem. Here are the key components:

app.modules.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TesterComponent } from './tester/tester.component';

@NgModule({
  declarations: [
    AppComponent,
    TesterComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

tester.component.ts:

import { Component, OnInit } from '@angular/core';
import { HttpHeaders, HttpParams, HttpResponse, HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Component({
  selector: 'app-tester',
  templateUrl: './tester.component.html',
  styleUrls: ['./tester.component.css']
})
export class TesterComponent implements OnInit {

  _url: string = "https://api.nasa.gov/planetary/apod";
  _api_key: string="DEMO_KEY";

  //
_title: string;
_date: string;

  constructor(private _httpClient: HttpClient) { }

  ngOnInit(): void {

    this.GetData(this._url).subscribe(()  =>
    {
       // do other stuff
     
    });
  }

  
 sendGetRequest(getUrl: string, headers: HttpHeaders, urlParams: HttpParams) : Observable<HttpResponse<Object>>{
    return this._httpClient.get<HttpResponse<Object>>(getUrl, {headers: headers, params: urlParams, observe: 'response'});
  }


  GetData(url: string)
  {

    const params = new HttpParams()
      .set("api_key", this._api_key);

    return this.sendGetRequest(url, headers, params).pipe(
      
      tap( response =>
      {
      
      console.log("returning data");

      if (response.headers)
      {
        console.log('headers', response.headers);
      }

      const keys = response.headers.keys();

      if (keys)
      {
        const headerKeys = keys.map(key =>
          `${key}: ${response.headers.get(key)}`);

        console.log('headerKeys', headerKeys);
      }

      this._date = response.body['date'];
      this._title = response.body['title'];
    },
    err => {
      console.log(err);
    }
    
      ));
  }

}

Addendum: To further illustrate the problem here is a small Java 11 program that calls exactly the same REST API with the same credentials. You can see from the output that the REST API is sending back all of the header response information. The question remains, why can't the Angular program calling exactly the same REST API see the full response headers? Is there some setting/flag/voodoo missing from the call?

Java 11 app:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;

public class MainClass {

    public static void main(String[] args) {
        String api_key = "DEMO_KEY";
        String uri = "https://api.nasa.gov/planetary/apod";

        uri += "?api_key=" + api_key;

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)).build();

        HttpResponse<String> response = null;

        try {
            response = client.send(request, BodyHandlers.ofString());
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("------------------");
        System.out.println("response.headers: " + response.headers());
        System.out.println(response.body());
        System.out.println("------------------");
    }

}

Output from Java app (response.header only for brevity):

response.headers: java.net.http.HttpHeaders@96f4f3fc { {access-control-allow-origin=[*], age=[0], connection=[keep-alive], content-length=[1302], content-type=[application/json], date=[Wed, 08 Jul 2020 17:13:42 GMT], server=[openresty], strict-transport-security=[max-age=31536000; preload], vary=[Accept-Encoding], via=[http/1.1 api-umbrella (ApacheTrafficServer [cMsSf ])], x-cache=[MISS], x-ratelimit-limit=[40], x-ratelimit-remaining=[39]} }

Thanks for your help!

Ken
  • 526
  • 6
  • 13

3 Answers3

4

You will need to set Access-Control-Expose-Headers header to include the additional headers. See Mozilla docs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers

By default, only the 7 CORS-safelisted response headers are exposed:

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

You will see your other headers in the network tab, but JavaScript will not have access to them unless they are in the Access-Control-Expose-Headers header.

Heres a similar question for Axios, a different javascript HTTP library from 4 years ago with the same answer: Axios get access to response header fields

And and Angular one from 2.5 years ago Read response headers from API response - Angular 5 + TypeScript

UPDATE: Since you are calling an API that I assume you don't own (https://api.nasa.gov/), you'll have to have NASA add the header Access-Control-Expose-Headers: X-RateLimit-Limit, X-RateLimit-Remaining if you need to read those headers in your client.

Your other option is to create a proxy server and instead of calling NASA, you call your own server which will call NASA and can include your rate limit headers.

cjd82187
  • 3,468
  • 3
  • 15
  • 19
  • Thanks for the reply. Unfortunatly I've tried that and it had no effect. To the code above, I added the following: const headers= new HttpHeaders() .set('Access-Control-Expose-Headers', 'Server'); I also tried various values of known headers that are in the browser debugger. – Ken Jul 07 '20 at 23:49
  • @Ken setting that in the client does nothing, you need to set it on the server and the server has to send that header in the response. – cjd82187 Jul 08 '20 at 11:50
  • Here's the deal though, the server must be sending the headers because, 1) the headers are in the browser debugger Network=> Response Headers, and 2) the exact same REST API call from a small Java app returns all the headers. It looks like it's not a server side problem but some kind of Angular problem in how it handles the "lazy loading" of the response headers. It's the Angular problem that I'm trying to find a solution to. – Ken Jul 08 '20 at 13:57
  • Unless your server is sending a header that says "Access-Control-Expose-Headers": "some-header, another-header, third-header", it will not be able to get processed by javascript, even if they send values for those headers. It is a browser security/CORS related feature. – cjd82187 Jul 08 '20 at 14:33
  • @ cjd82187 I added a small Java program that further demonstrates the problem to the question. As you can see from the output, the REST call is returning all of the headers. If Access-Control-Expose-Headers were not enabled neither the Java program nor the the Angular program would be able to get to the headers. Clearly the Java program has access to the headers, where the Angular program does not. I certain looks like the REST server is sending all the headers. What is the missing step on the Angular program side of things that would give access to the full headers? – Ken Jul 08 '20 at 18:02
  • The java program is not a web browser. It is a web browser security feature. I had the same exact problem at work. Read the link I posted from Mozilla, it explains in detail that you will NOT be able to read any header other than those 7 on a CORS request UNLESS its listed in a Access-Control-Expose-Headers sent from the server. You WILL see it in the browser network tab, but JS cannot access it. – cjd82187 Jul 08 '20 at 18:23
  • I updated the image in the question. When I query the OPTIONS I get back Access-Control-Expose-Headers, which would indicate that the headers should be coming back right? So, how does one get the headers out of the response? Is there some setting on the Angular lazy loading? – Ken Jul 08 '20 at 18:41
  • Your update is still wrong, You are sending the header "Access-Control-ALLOW-Headers" with a value of "Access-Control-Expose-Headers". You need to send a header of "Access-Control-Expose-Headers" with a value of all the headers you want to be able to read, comma separated. – cjd82187 Jul 08 '20 at 18:55
  • Nope, I sent this: const headers= new HttpHeaders() .set('Access-Control-Expose-Headers', 'X-RateLimit-Remaining') ;- the response in the image came entirely from the server as Access-Control-Allow-Headers : Access-Control-Expose-Headers – Ken Jul 08 '20 at 19:01
  • The server has to set the header, not angular. – cjd82187 Jul 08 '20 at 19:04
  • Right. Given that the server is sending the headers, what do I need to do on the Angular end to pull them out of the response? I've tried all sorts of things including setting up an HttpInterceptor and looking for the headers there. There must be some way to get to them, after all I can see them right in the response.headers. Seriously, there should be some mechanism to dig out the entire response header within Angular. – Ken Jul 08 '20 at 19:19
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/217482/discussion-between-cjd82187-and-ken). – cjd82187 Jul 08 '20 at 19:51
0

For getting the full headers of the HTTP response along with the content, you are suppose to pass another parameter called observe while making the request

http
  .get<MyJsonData>('/data.json', {observe: 'response'})
  .subscribe(response => {
      response.headers.keys().map( (key) => console.log(`${key}: ${response.headers.get(key)}`));

 });
sunilbaba
  • 441
  • 2
  • 9
  • 1
    Thanks for the reply. The {observe: 'response'} option is already set in my sample above. Even with that option the full set of headers is not being returned in Angular calls. – Ken Jul 07 '20 at 14:31
  • 1
    exposed the header key from server side using access-control-expose-headers – sunilbaba Jul 07 '20 at 15:45
  • I don't see this header in your screenshot.. can you confirm that – sunilbaba Jul 07 '20 at 15:46
  • In that 1 & 2 I don't see expose headers – sunilbaba Jul 08 '20 at 04:03
  • @ sunilbaba I not sure what you mean. In 1) the debugger Network=> Response Headers clearly shows that the headers are in the response, in 2) Java is clearly returning all the headers. The headers are available, but something in Angular is preventing access to all but one response header field. That's the problem we're trying to solve. – Ken Jul 08 '20 at 14:03
  • I understand exactly what you are trying to solve my dear friend. If you see in your response header there is a property called access control allow origin. Similarly there should be another header called access control expose header to access extra header. – sunilbaba Jul 08 '20 at 17:41
  • please see last comment to @ cjd82187 – Ken Jul 08 '20 at 18:05
-2

from mozila docs

"The Access-Control-Expose-Headers response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request.

Only the CORS-safelisted response headers are exposed by default. For clients to be able to access other headers, the server must list them using the Access-Control-Expose-Headers header."

you need to config the server side api (core.api, exc')

explicit allow your header you want to expose to the client

services.AddCors(options =>
{
    options.AddPolicy("AllowAll", builder =>

    {
        builder.AllowAnyHeader()
               .AllowAnyOrigin()
               .WithExposedHeaders("Content-Range"); 
    });
});

after you config you will see the header "content-Range"

content-range: Categories 0-2/4

and it will be available in the JavaScript code

David Buck
  • 3,752
  • 35
  • 31
  • 35
  • Down voted because as previously discussed this is not my API it is controlled by another entity. Also, the question is regarding why Java can see all of the headers, yet Angular cannot using the same API call. – Ken Apr 13 '21 at 14:34