Angular: Custom headers are ignored by $http and $resource. Why?
Asked Answered
C

3

9

I'm trying to access a REST service I don't control. First problem is that the service doesn't include a Access-Control-Allow-Origin header, which is a problem that, if I understand correctly, immediately limits me to JSONP.

Also, by default, this service sends XML rather than JSON, though it's capable of sending JSON. I think it should respond to my Accept header, the people responsible for the service say it looks at my Content-Type. That would mean I'd need to do a POST rather than a GET (though get makes more sense when I'm just getting some static data, right?).

Stubborn as I am, I'm trying my Accept header first. Since Angular only accepts JSON, I'd expect it to use the Accept: application/json header by default, but it doesn't, and it ignores my attempts to set it manually:

app.config(['$httpProvider', function($httpProvider){
    console.log($httpProvider.defaults.headers.common);
    delete $httpProvider.defaults.headers.common['X-Requested-With'];
    $httpProvider.defaults.headers.post['Accept'] = 'application/json, text/javascript';
    $httpProvider.defaults.headers.post['Content-Type'] = 'application/json; charset=utf-8';
    $httpProvider.defaults.headers.post['Access-Control-Max-Age'] = '1728000';
    $httpProvider.defaults.headers.common['Access-Control-Max-Age'] = '1728000';
    $httpProvider.defaults.headers.common['Accept'] = 'application/json, text/javascript';
    $httpProvider.defaults.headers.common['Content-Type'] = 'application/json; charset=utf-8';
    $httpProvider.defaults.useXDomain = true;
}]);

I do this again in the actual resource:

return $resource('http://foo.com/getStuff', {}, {
    fetch: {
        method:'JSONP',
        params: params,
        headers: {
            'Accept':'application/json, text/javascript',
            'Content-Type':'application/json; charset=utf-8'
        },
        isArray:false,
        callback: 'JSON_CALLBACK'
    }
});

But still, the request headers contain Accept: */*.

My question is: WHY? Why does Angular ignore my headers? And how do I get it to use the proper headers anyway?

And also: is there a way to use JSONP in a POST?

Edit: Originally I used Angular 1.0.7, but I just tried it with 1.2.3 and got the same results. Headers are ignored, yet everybody claims that this is the way to do it.

I also tried doing it directly with $http, rather than with $resource, with the same results.

Edit 2: Here's a JSFiddle. It's anonymized and doesn't use my real server, but using Firebug/developer tools, you can verify that it sends Accept: */* on both calls, despite my many attempts to set application/json headers. And that is my real problem here. On my real server, I'm getting an XML result because of that, despite my real server's ability to send JSON.

(Whether the real server supports jsonp is less relevant at the moment. This dummy server clearly doesn't, but that's okay. I just care about the headers.)

Edit 3: I've tried both solutions suggested below:

$http.defaults.headers.common['Accept'] = 'application/json, text/javascript';

$http.defaults.transformRequest.push(function (data, headersGetter) {
    headersGetter().Accept = "application/json, text/javascript";
    return data;
});

I've tried both statements separately. In the controller, and then in the service just before the http call itself. Still doesn't work.

Can someone give me a JsFiddle where this is shown to work?

Edit 4: I notice that when I use GET rather than JSONP, the Accept header is correct. But then the response is rejected because it doesn't have the correct header.

What kind of headers should a JSONP call have? Because there's a lot more headers in the JSONP call, but nothing that identifies it as JSONP. Does the server have to have explicit JSONP support for this to work? I suddenly realize I don't know nearly enough about jsonp.

Coelostat answered 28/11, 2013 at 12:42 Comment(10)
Are you sure that the WebService you are using responds with valid JSONP to your request ? First thing I noticed, is that you don't set your callback query param correctly. For $resource should be rather: ..., params: {callback: 'JSON_CALLBACK'}, ... Also please provide a Plunk or JSFiddle if possible, so we can play around with your code.Physic
I'm sure it doesn't respond with valid JSONP. It sends me XML, presumably because I'm not sending the correct headers. My main issue at this moment is setting the headers correctly so the service knows to send me json or jsonp.Coelostat
I know, that's why I suggested looking at your REQUEST URL at the first place (-:. Each WebService responding with JSONP is searching for callback in URL query string and will never ever respond with JSONP since it needs callback param value to do that. It basically extracts callback param value and sends: <<callback_param_value>>(<<response_JSON_object>>); back to you as application/javascriptPhysic
I added a JSFiddle. The real problem is that it sends the wrong headers. After that, I can probably figure out the JSONP stuff myself, although the way I'm doing it here is how the documentation tells me to do it. Then again, the same is true for the headers, and that clearly doesn't workCoelostat
We had a similar problem and ended up putting the default headers declaration in a base controller that all of the other controllers in the page are a child of. The base controller is applied to the HTML tag in our case, but you could probably put it on the BODY tag and be fine. When you do it that way, the assignment changes from $httpProvider to just $http, and then everything else is the same. Is this something that you are able to work with?Interrelated
@MBielksi I will try that as soon as I can. Put it in an answer. If it works and I don't get a better answer before the bounty runs out, the bounty is yours. I'd rather not put this in the controller, but a workaround is a lot better than nothing.Coelostat
@Coelostat You can't set headers or use POST method for JSONP requests.Snead
@Snead That seems to be the unfortunate answer to my question. I wish it had been a bit more clear, as I've wasted a lot of time on figuring this out. At least I learned a lot about what JSONP isn't.Coelostat
JSONP is just a hackish and obsolete way of doing CORS :)Snead
@Snead That's an excellent summary of what I learned over the past week.Coelostat
R
12

I think your answer is here. According to the wiki, A JSONP call is executed through injection of a <script> tag to load the script from the host server, which responds by calling your callback, passing the data. A <script> tag generates a regular browser request (not an XmlHttpRequest), and the browser will send its own Accept header (it also sends its own User-Agent header, for example).

I would hope there is an easier client-side way to do this, but I think the only way may be the one suggested in the referenced post:

So, if you want to be able to set request headers for cross domain calls you will have to setup a server side script on your domain that will delegate the call to the remote domain (and set the respective headers) and then send the AJAX request to your script.

EDIT: here is a (rejected) jQuery bug report about this same problem.

Some more background info:

In angular, callbacks are managed automagically, so if your say this:

$http({
    method: "JSONP",
    url: "http://headers.jsontest.com?callback=JSON_CALLBACK",
}).success(function(data) {
    console.log('Return value:');
    console.log(data);
}).error(function(data) {
    console.log('Error!');
    console.log(data);
})

a <script> tag will be created that looks more or less like this:

<script type="application/javascript"
        src="http://headers.jsontest.com/?callback=angular.callbacks._1">
</script>

The content of the response to http://headers.jsontest.com/?callback=angular.callbacks._1 will be:

angular.callbacks._1({key1: "value1", key2: "value2"});

angular.callbacks._1 will contain your success function, and it will be called with the data.

Randle answered 5/12, 2013 at 15:53 Comment(2)
Thanks for the extensive explanation. I'd kinda cobbled together this information over the past week, but it's good to have it explicit and all in one place. Somehow I was under the impression that the client somehow handled the JSONP stuff, but it's clear to me now that the server needs to be able to handle it. And I guess that also means that JSONP is obsolete, because if you can add JSONP support to the server, it's probably easier to add the Access-Control-Allow-Origin header to your response.Coelostat
It also makes the "JSON Vulnerability Protection" at docs.angularjs.org/api/ng.$http rather meaningless, IMO. (That was the bit that led me to believe that you could use JSONP on a server against the server-owner's wishes, creating the need for extra protection.)Coelostat
I
0

While what you have is supposed to work according to the docs, my experience has been a bit different. To get around this issue, we did the following:

Create a "base controller" that gets added to the page either on the body or html tag. In that controller, make the assignment using $http instead of $httpProvider. Because your base controller loads when the initial page loads, it is there for all other controllers and services that will run in your app.

I don't know why this works and the proscribed method does not, and I'd love to see an answer to your question that is better than this work-around, but at least this can get you moving forward with development again.

Interrelated answered 4/12, 2013 at 15:12 Comment(2)
Doesn't seem to work. Can you show this in a JsFiddle, perhaps? In mine, if I add this either in the controller or in the service, I still get Accept: */*.Coelostat
JsFiddle and I have a love/hate relationship today. While trying to get a fiddle to work, I noticed in the Angular docs that, in addition to them listing that the defaults are exactly what you want, they are setting things with an object instead of individually: $httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. Maybe you should check your server to see if those accepts types are being handled properly? We ran into this same situation very early on and that ended up being the case.Interrelated
F
0

The following works for me - however, I do that during "runtime" with $http and I am not using $httpProvider during bootstrapping.

function SomeCtrl($http) {

    $http.defaults.transformRequest.push(function (data, headersGetter) {
        headersGetter().Accept = "application/json, text/javascript";
        return data;
    });

}

Edit

Here is a working jsFiddle version. Check the request which is done with Developer Tools/Firebug and see that "application/json, text/javascript" is requested.

Foal answered 4/12, 2013 at 16:27 Comment(3)
Wait, what does transformRequest do? This looks like a third way to handle it, though it sounds very similar to MBielski's solution.Coelostat
@Coelostat I added a fiddle jsFiddle that is working with this version.Foal
But you do a get, rather than a jsonp. With jsonp, your fiddle loses the header too. When I do I get, I also get the correct headers, but I'm trying to get jsonp to work.Coelostat

© 2022 - 2024 — McMap. All rights reserved.