angular2/http get location header of 201 response
Asked Answered
I

2

10

I finished successfully the angular2 "tour of heroes" starter tutorial. Then I build an api based on symfony3, FosRestBundle and BazingaHateoasBundle as backend. Now nearly everything works, but on creating new items I'm not able to get the "Location" header from the response to load the newly created Item.

Here is my Hero Service:

import {Injectable} from "angular2/core";
import {Hero} from "./hero";
import {Http, Headers, RequestOptions, Response} from "angular2/http";
import {Observable} from "rxjs/Observable";
import "rxjs/Rx";

@Injectable()
export class HeroService {

    constructor(private _http:Http) {
    }

    private _apiUrl = 'http://tour-of-heros.loc/app_dev.php/api'; // Base URL
    private _heroesUrl = this._apiUrl + '/heroes';  // URL to the hero api


    getHeroes() {
        return this._http.get(this._heroesUrl)
            .map(res => <Hero[]> res.json()._embedded.items)
            .do(data => console.log(data)) // eyeball results in the console
            .catch(this.handleError);
    }

    addHero(name:string):Observable<Hero> {

        let body = JSON.stringify({name});

        let headers = new Headers({'Content-Type': 'application/json'});
        let options = new RequestOptions({headers: headers});

        return this._http.post(this._heroesUrl, body, options)
            // Hero is created, but unable to get URL from the Location header!
            .map((res:Response) => this._http.get(res.headers.get('Location')).map((res:Response) => res.json()))
            .catch(this.handleError)

    }

    private handleError(error:Response) {
        // in a real world app, we may send the error to some remote logging infrastructure
        // instead of just logging it to the console
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }

    getHero(id:number) {
        return this._http.get(this._heroesUrl + '/' + id)
            .map(res => {
                return res.json();
            })
            .do(data => console.log(data)) // eyeball results in the console
            .catch(this.handleError);
    }
}

So when I'm calling the addHero method, a new Hero is created and a 201 response is returned without a body, but with the Location header set:

Cache-Control: no-cache
Connection: Keep-Alive
Content-Length: 0
Content-Type: application/json
Date: Tue, 29 Mar 2016 09:24:42 GMT
Keep-Alive: timeout=5, max=100
Location: http://tour-of-heros.loc/app_dev.php/api/heroes/3
Server: Apache/2.4.10 (Debian)
X-Debug-Token: 06323e
X-Debug-Token-Link: /app_dev.php/_profiler/06323e
access-control-allow-credentials: true
access-control-allow-origin: http://172.18.0.10:3000

The problem is, that the res.headers.get('Location') didn't get the Location header from the response. After some debugging it seems to be that the res.headers just provides two headers Cache-Control and Content-Type. But no Location.

I knew it should be also possible to solve the problem by adding the newly created data to body of the response, but this is not really the way I want to solve this.

Thank you in advance!

Interpenetrate answered 29/3, 2016 at 9:38 Comment(4)
Location: http://tour-of-heros.loc/app_dev.php/api/heroes/3. Does it contain by your response object? If it is, what is the problem?Stepchild
No, I didn't find it in the response object. I'm sure this is the problem ;). But I have no idea where else I have to search for it.Interpenetrate
So how did you get this last snippet of information?Stepchild
@Stepchild which information? From the browser console I get all headers, and the header for the angular response object I get from console.log(res); and console.log(res.headers.keys()) (That i did not mention in the question).Interpenetrate
M
13

You should use the flatMap operator instead of the map one:

return this._http.post(this._heroesUrl, body, options)
        .flatMap((res:Response) => {
          var location = res.headers.get('Location');
          return this._http.get(location);
        }).map((res:Response) => res.json()))
        .catch(this.handleError)

Edit

Regarding your header problem.

I think that your problem is related to CORS. I think that the preflighted request should return the headers to authorize in the Access-Control-Allow-Headers in its response. Something like that:

Access-Control-Allow-Headers:location

If you have the hand on the server side of the call, you could update this header with the headers you want to use on the client side (in your caseLocation`).

I debugged the request and specially within the XhrBackend class that actually executes the request and gets the response from the XHR object. The header isn't returned by the code: _xhr.getAllResponseHeaders(). In my case, I only have the Content-Type one.

See this link: https://github.com/angular/angular/blob/master/modules/angular2/src/http/backends/xhr_backend.ts#L42.

From the following question:

Cross-Origin Resource Sharing specification filters the headers that are exposed by getResponseHeader() for non same-origin requests. And that specification forbids access to any response header field other except the simple response header fields (i.e. Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, and Pragma):

See this question for more details:

Millar answered 29/3, 2016 at 10:38 Comment(3)
Thanks for your answer. But same problem, res.headers.get('Location') returns null.Interpenetrate
Your right! CORS is the problem. See my own answer. Many thanks!Interpenetrate
And your suggestion to use the flatMap is more than helpful to another problem :)Interpenetrate
I
6

My fault!

after reading https://github.com/angular/angular/issues/5237 and http://www.aaron-powell.com/posts/2013-11-28-accessing-location-header-in-cors-response.html, I recognize that there must be something wrong with my CORS configuration of the NelmioCorsBundle (https://github.com/nelmio/NelmioCorsBundle/issues/9). As a quick&dirty fix I added expose_headers: ['Origin','Accept','Content-Type', 'Location'] to the symfony configuration: Before:

nelmio_cors:
    paths:
        '^/api/':
            allow_credentials: true
            origin_regex: true
            allow_origin: ['*']
            allow_headers: ['Origin','Accept','Content-Type', 'Location']
            allow_methods: ['POST','GET','DELETE','PUT','OPTIONS']
            max_age: 3600

After:

nelmio_cors:
    paths:
        '^/api/':
            allow_credentials: true
            origin_regex: true
            allow_origin: ['*']
            allow_headers: ['Origin','Accept','Content-Type', 'Location']
            allow_methods: ['POST','GET','DELETE','PUT','OPTIONS']
            expose_headers: ['Origin','Accept','Content-Type', 'Location']
            max_age: 3600

And now it works like a charm.

So it is not an angular issue and was never really solvable. Sorry! I will keep this for everyone who stumbled over a similar CORS problem.

Many thanks to all who contributed

Interpenetrate answered 29/3, 2016 at 11:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.