How to override the adapter headers that get sent in the ajax request?

I came across some weird behavior a couple of days ago while using ember-data(I’m using 3.12 without ember-fetch or ember-ajax).

So I have this headers object in my application adapter that gets sent in almost all of the requests I make in my application.

// Application adapter
export default Component.extend({
  headers: {
   Accept: "application/json" // Majority of my requests will accept json content
  }
  ... Other properties and method go here.
})

What I’m trying to accomplish is to override the accept header from application/json to text/csv because there is a method in my application that downloads the content I receive from the server as csv. This is how I’m doing it.

saveAsCSV() {
  const options = {
    headers: {Accept: "text/csv"} // I want this request to use text/csv because we are downloading content as csv in this method.
  }
  
  this.store.adapterFor("Application").ajax(this.url, "GET",  options).then((response) => {
     // Do something with the response
  })
}

Unfortunately this doesn’t do what I expect. The request headers will still have application/json as the accept type. This is because of what happens here https://github.com/emberjs/data/blob/master/packages/adapter/addon/rest.js. In particular, options.headers = assign({}, options.headers, headers); Here headers is the headers from the application adapter in my case.

To me this particular behavior is not what I expect and maybe someone can explain to me why it is done like this. To me, the headers in any adapter are headers that meant to be applied to a majority of your requests but can be overridden when you pass in headers to the ajax method. Not that the headers in the adapter can override the headers that I provided to the ajax method.

How should I proceed to get the desired behavior?

I don’t know why it works that way. But I would solve this by implementing my own findRecord (or findAll or query, etc, depending on what you’re trying to customize).

Also, it’s usually a bad idea to introduce custom methods on adapters. The adapters are supposed to be called by the store and nobody else. Instead you can implement query on the adapter and it can receive whatever options you pass to store.queryRecord's `adapterOptions argument.

On the other hand, if you’re not actually trying to get back a Model from this request, it probably doesn’t need to go through ember data at all, and I would just fetch it.

Out of curiosity what would you recommend as an alternative for the case where you have an operation that’s model-related but doesn’t quite fit within the scope of Ember Data methods? For example at my company we use adapter methods for a few one-off batch-type operations, like POST /users/:id/permissions. Obviously we could just use a service but I think the adapter made sense because then it’s easy to still use the URL building and some of the other related request-making code.

It depends on the example. Often it’s easy enough to make up a new resource:

await store.createRecord('user-permissions', { ... })

Or you can customize the query (or queryRecord) method when a particular argument is passed to it, which can do basically whatever you want it to do:

await store.queryRecord('users', { 
  adapterOptions: { 
    updatePermissions: { user_id: 1, newPerms: ... }
  }
});
1 Like

That still seems a little more roundabout to me (using store.queryRecord when the use case is more like store.batchCreate but I guess it’s not necessarily weirder than calling adapterFor in the middle of app code so… I’ll take it. Thanks!

Ya I would opt into using fetch since the csv text that gets returned doesn’t get stored in the model but the custom method in my adapter is still used to build the url(@dknutsen mentioned it as one of his use cases which I agreed with). Any tips on handling that? also I was bit surprised to hear that it is a bad idea to include custom methods in the adapter since this article https://emberigniter.com/non-standard-rest-actions-ember-data/ showed in approach 2 the benefits of including custom methods in your adapter i.e “We miss out on configuration parameters already available to the adapter: namespace, host, custom headers and so on”. Can you please explain further why it is a bad idea to introduce custom methods in the adapter.

This is my own opinion, but I don’t like adding custom methods on the adapter because it’s an abstraction violation. The app talks to the store, the store manages state and caching, and the store talks to the adapters as needed. Ideally, all caching and state management policy happens in the store, and that policy holds because nobody is going around the store directly to the adapters to manipulate model state.

I understand what you mean about already having configuration and URL generation capabilities in the adapter. But those can be factored out of the adapter into a plain javascript module that’s used by both the adapter and other fetch-based code.

1 Like

To give a little perspective from the data-team, as I believe we differ from Ed here:

Adapters are EmberData’s current abstraction for “managed fetch”. Using them directly for managed fetch when the request or response don’t align naturally to a Model is encouraged. Unfortunately EmberData historically encouraged over-modeling when no Model is needed, and this is one area we’ve been actively working to reverse.

New API docs for Adapters are in active development, and this pattern of using adapters is a thing they address.

Going forward, the data team has been working on an enhancement to the adapter/serializer paradigm that aligns it more closely to a decoration over fetch for managing requests stand-alone from the cache and presentation layers which EmberData also provides.

2 Likes

I agree this is unexpected, and probably should be considered a bug. I opened this issue for discussion: https://github.com/emberjs/data/issues/6588

1 Like

Thanks for opening up a discussion for this on github. For now my solution to this problem is to override the ajaxOptions function in the adapter.

ajaxOptions(url, type, options) {
  const hash = this._super(url, type, options);

  // Checking if headers were passed to the ajax method. Override the adapter headers if that is the case.
  if(options.headers) {
   // Doing it like this ensures that the adapter.headers are not completely changed.
    hash.headers = assign({}, hash.headers, options.headers);
  }

  return hash;
}

Not ideal because this method is private and weird that headers are changed twice but this is the only way I was able to achieve my goal.