5

I use msw to mock backend responses for automated FE tests.

I wanted to create an Export Functionality for user-created data. I am POSTing a Request to the desired route, and want to return a response with a .csv ; .pdf ; .xls file which should be downloaded automatically.

I've been searching for appropriate ways to do that and came across the Content-Disposition Response Header.

Sadly the response doesn't trigger a save dialoge, nor start a download, but I can't find the mistake.


EDIT: After having a few conversations and the somewhat unique circumstances, I settled for creating a ObjectURL for the Blob-Data generated, and force a download with the window.open() call.

I changed the expected Response in my Handlers and added a corresponding call in my FE.

handlers.js

rest.post('/mock/things/:thingId/export', async (req, res, ctx) => {
  const docType = req.headers.get('Content-type');
  const startDate = req.url.searchParams.get('startDate');
  const endDate = req.url.searchParams.get('endDate');
  const selected = req.body;

  const resDataString = 'testName,testLastName,\r\nDaniel,Schaller';
  const blob = new Blob([resDataString], { type: docType });
  const buffer = await blob.arrayBuffer();

  return res(
    ctx.status(200),
    ctx.set({
      'content-type': `${docType}`,
      'Content-length': buffer.byteLength.toString(),
    }),
    ctx.body(buffer),
    ctx.delay(),
  );
}),

store-module.js

async exportThings(ctx, exportData) {
  const {
    docType, startDate, endDate, selected,
  } = exportData;

  const res = await services.exportThingsForStuff(ctx.state.id, docType, startDate, endDate, selected);

  if (res.data !== undefined) {
    const dataBLOB = new Blob([res.data], { type: res.headers['content-type'] });
    window.open(URL.createObjectURL(dataBLOB));
  }
},

Let me show you the workflow:

some-component.vue

async submitExport() {
  const exportDataFilters = {
    docType: this.docType,
    startDate: this.startDate,
    endDate: this.endDate,
    selected: this.selected,
  };

  const response = await this.$store.dispatch('exportThings', exportDataFilters);
  console.log(response);
},

store-module.js

async exportThings(ctx, exportData) {
  const {
    docType, startDate, endDate, selected,
  } = exportData;

  return services.exportThingsForStuff(ctx.state.id, docType, startDate, endDate, selected);
},

services.js

export const exportThingsForStuff = async (thingsId, docType, startDate, endDate, pl) => axios.post(`/mock/things/${thingsId}/export`, pl, {
  headers: {
    'Content-type': docType,
  },
  params: {
    startDate,
    endDate,
  },

And Finally the MSW Response function from handlers.js

rest.post('/mock/things/thingId/export', (req, res, ctx) => {
  // Vars will be in use to mimic filter behavior, as soon as the download problem is solved
  const docType = req.headers.get('Content-type');
  const startDate = req.url.searchParams.get('startDate');
  const endDate = req.url.searchParams.get('endDate');
  const selected = req.body;


  
// Creating a test string and blobbing it.
const resDataString = 'testName,testLastName,\r\nJoe,Moe';
const blob = new Blob([resDataString], { type: 'text/csv' });

return res(
  ctx.status(200),
  ctx.set({
    'Content-Disposition': 'attachment; filename="things_export.csv"',
    'Content-Length': blob.size.toString(),
    'Content-Transfer-Encoding': 'binary',
    'Content-Type': 'text/csv',
  }),
  ctx.body(blob),
  ctx.delay(),
);

}),

I can't find the problem with it, since the response looks like expected:

Request URL: http://localhost:8080/mock/things/101/export?startDate=&endDate=
Request Method: POST
Status Code: 200 OK (from service worker)
Referrer Policy: strict-origin-when-cross-origin

RESPONSE HEADERS
content-disposition: attachment; filename="things_export.csv"
content-length: 37
content-transfer-encoding: binary
content-type: text/csv
x-powered-by: msw
D.Schaller
  • 611
  • 3
  • 19
  • Can you try sending text in `ctx.body` instead of a Blob? – kettanaito Oct 16 '21 at 19:36
  • @kettanaito I tried pretty much everything. HTML, plain Text, stringified HTML and blobs. Sadly the outcome doesn't change. I get the response with the proper data in each case, yet the download doesn't trigger as expected. – D.Schaller Oct 18 '21 at 06:18
  • `content-transfer-encoding` is a MIME header, not an http one. To force a download your content-type header value should be `application/octet-stream` – miknik Oct 19 '21 at 23:24
  • Please, could you trying providing [`responseType: 'blob'`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) when [configuring](https://axios-http.com/docs/req_config) the axios `POST` in the `exportThingsForStuff` method: `export const exportThingsForStuff = async (thingsId, docType, startDate, endDate, pl) => axios.post(`/mock/things/${thingsId}/export`, pl, { headers: { 'Content-type': docType, }, params: { startDate, endDate, }, responseType: 'blob'`. Please, could you try? – jccampanero Oct 21 '21 at 22:15
  • Please, consider review [this related so question](https://stackoverflow.com/questions/41938718/how-to-download-files-using-axios) as well, I think it can be of help – jccampanero Oct 21 '21 at 22:15
  • @miknik Does not work. And in combination with `Content-Disposition: attachment` the application/octet-stream should not be necessary. Since I know the given Data-Type of my response data, application/octet-stream is not the right choice imo. – D.Schaller Oct 22 '21 at 08:52
  • @jccampanero The response-type didn't change the behaviour, but thanks for the heads up. I looked into the linked question, but I really don't want to programatically append and click a link for the sole purpose of downloading a file. – D.Schaller Oct 22 '21 at 08:54
  • You are welcome @D.Schaller. I am very sorry to see that the advice was not helpful. – jccampanero Oct 22 '21 at 09:01
  • @D.Schaller I honestly have never worked with that mocking library, but have you tested [this](https://mswjs.io/docs/recipes/binary-response-type) approach for returning the binary information? – jccampanero Oct 22 '21 at 09:07
  • @jccampanero Yes I did. No it does not work. I tested all I can think of (including the approaches from documentations and various SO posts). – D.Schaller Oct 22 '21 at 10:10
  • I see @D.Schaller. I think the main issue is dealing with the download in the context of the test; I mean, I think the anchor solution proposed in the related SO question can be a good approach for instance if running the application in the browser. If you have access to the response blob in your code, a similar but different approach I followed sometimes is the following: `const blobUrl = URL.createObjectURL(blob); window.open(blobUrl);`, being `blob` the returned blob in your response. The problem is that the user experience is worse because you lost the ability of naming your file – jccampanero Oct 22 '21 at 10:28
  • @jccampanero Tried using this method, it might actually work. naming the file is not necessarily needed in my use case. Testing it I stumbled across a somewhat strange behaviour. The response seems to always be converted to a string, hence losing whatever I did with it before. I'm sending a blob, but the response data is neither binary, nor a blob, but the fully evaluated data as a string. I'm wondering if that is the reason why nothing really works regarding 'Content-Disposition'. Maybe I have to switch the Mocking Library... – D.Schaller Oct 22 '21 at 11:31

2 Answers2

0

As far as I understood you are consuming the response from that link your Javascript application, right? In that case browser won't do open download dialog. You should redirect user to a link to trigger download dialog. But you want to use a POST method, than fill in a form and submit it programmatically instead of calling axios.post

laltin
  • 1,134
  • 12
  • 20
  • Thanks for your attempt to provide an answer. I settled for the option of creating an ObjectURL and opening it with `window.open(ObjectURL)` since initial naming of the file isn't needed. – D.Schaller Oct 26 '21 at 07:42
0

I think the problem here is related to this answer

I guess it's a browser feature...

I don't like download it with URL.createObjectURL(blob) too.
seems I have to use the old school HTML form element or HTTP get

thanks for the precise description though.
I have the same issue and your question direct me to the related answer above, which really helps!

wong2win
  • 1
  • 1