How to set the `Accept-Language` header for one test in Playwright? - playwright-test

How can I set the Accept-Language header for one test call
in Playwright ?
E.g.:
import { test, expect } from '#playwright/test';
test( 'language is interpreted correctly', async function({ page, context }) {
// -----
// HERE: Set the `Accept-Language` header to 'de-DE,de;q=0.9,en;q=0.8' somehow ?
// -----
const response = await page.goto( 'http://example.com' );
const acceptLanguageHeader = await response.request().headerValue('Accept-Language');
expect( acceptLanguageHeader ).toBe( 'de-DE,de;q=0.9,en;q=0.8' );
expect( somethingThatDependsOnTheLanguage ).toBe( true );
});
What I found, which doesn't work:
I know I can set extra headers with page.setExtraHTTPHeaders(),
but that apparently doesn't overwrite the Accept-Language header.
I know I can configure TestOptions with:
playwright.config.js for the default context, or
testConfig.use or testProject.use for all tests,
but I want to test different settings in each test() call.
E.g. this doesn't work:
test( 'try modified accept-language header', async function({ page, context }) {
await context.setExtraHTTPHeaders({
'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8',
'some-extra-header': 'some extra header value from context',
});
await page.setExtraHTTPHeaders({
'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8',
'some-extra-header': 'some extra header value from page',
});
const response = await page.goto( 'http://example.com' );
console.log('headers:', response.request().headers()); // <-- 'accept-language' is 'en-US',
const acceptLanguageHeader = await response.request().headerValue('Accept-Language');
expect( acceptLanguageHeader ).toBe( 'de-DE,de;q=0.9,en;q=0.8' ); // <-- Received: "en-US"
});
I have seen the answers to "How to add custom headers ..."
and "How to change default language ..." and other examples,
but
they are creating their own browser
and browser.context (like const context = await browser.newContext( ... )),
and
they are not using a test call, and
don't show where they get the browser instance from.
(Probably they are not using the playwright test-runner. I don't know how they are running their tests)

You can use browserContext.route()
to modify network requests in each test individually.
E.g.:
test( 'Example for browserContext.route()', async function({ page, context }) {
// -- modify the request
await context.route('**/*', (route, request) => {
route.continue({
headers: {
...request.headers(),
'accept-language': 'de-DE,de;q=0.9,en;q=0.8',
}
});
});
// -- navigate
await page.goto( urlOfMyWebsite );
// -- test if language of accept-header is correctly used
expect( someContent ).toBe( translatedToGerman );
});
RegExp is also allowed to match the url, e.g.:
await context.route(/^\/[-a-z]+$/, (route, request) => { ...
Note that the header name seems to be case-sensitive, and might behave weird.
I found different tests behave in different ways, for example (I could not always reproduce this in different environments):
setting only 'Accept-Language': 'de-DE' might result in e.g. Received: "en-US, de-DE".
setting both: 'Accept-Language': 'de-DE', 'accept-language': 'de-DE' might result in Received: "de-DE, de-DE".

Related

How can I reach the 'Retry-After' response header using axios?

I'm building a simple Vue2 app with Auth section, which makes requests to REST API service.
So, I have my axios instance:
const instance = axios.create({
baseURL: BASE_URL,
timeout: DEFAULT_TIMEOUT,
withCredentials: true,
headers: {
accept: 'application/json',
},
});
To make authorization requests I use a separate module:
const auth = (api) => ({
submitPhoneNumber({ userPhone }) {
return api.get(`auth/${userPhone}`);
},
});
And set it all up together like this:
export default {
auth: auth(instance),
};
Then I add my api to Vue as a plugin:
export default {
install(Vue) {
const vueInstance = Vue;
vueInstance.prototype.$api = api;
},
};
In the component I access my api-plugin and make a request, extracting status and headers from it:
const { status, headers } = await this.$api.auth.submitPhoneNumber({
userPhone: this.userPhone,
});
When I look through the response in chrome devtools, I clearly see a "retry-after" header with number of seconds, after which I can make another request.
Upon receiving the response, I would like to save this number of seconds to some variable and then render a warning message like "Please wait { seconds } to make another submit".
The problem is that in my code I have no such header in the response (while I can see it in devtools, a I said):
see the screenshot
So, when logging the headers from my response, there are just these:
{content-length: '19', content-type: 'application/json; charset=utf-8'}
What is the problem with that?
Try var retrysec = error.response.data.retry_after that worked for me

How to read and write Data in Airtable with Flutter?

dependencies:
airtable: ^0.0.2
import 'package:airtable/airtable.dart'; ??
import 'package:dart_airtable/dart_airtable.dart'; ??
void main() async {
final apiKey = 'my-airtable-api-key'
final projectBase = 'my-airtable-project-base';
final recordName = 'Tasks';
var airtable = Airtable(apiKey: apiKey, projectBase: projectBase);
var records = await airtable.getAllRecords(recordName);
print(records);
}
If anyone knows how to solve it, I will be very grateful.
First of all, it seems like you mix two things together. Currently there are two packages that provide a library for the communication with airtable: airtable 0.0.2 and dart_airtable 0.1.1.
In your example code you use the import statement for the first package but the code from the second one. As the first one doesn't provide a valid link to a repository I'll have a look at the second one.
To retrieve records for a project from the API you can use this code:
var records = await airtable.getAllRecords(recordName);
Unfortunately the package has some bugs. As I wanted to create a record I used the method createRecord like this:
var test = await airtable.createRecord(
recordName,
AirtableRecord(
fields: [
AirtableRecordField(
fieldName: 'Short Description',
value: 'Test Description',
),
],
),
);
print(test);
But the only response I got was null. So I cloned the project and debugged the method. The result was this:
{"error":{"type":"INVALID_REQUEST_UNKNOWN","message":"Invalid request: parameter validation failed. Check your request data."}}
After a little bit of searching I found out that the package sends wrong data to the airtable API. In a toJson method of the AirtableRecord model, the author added the unnecessary fields id and createdTime. After deleting them, the method works.
Response:
AirtableRecord(id: rec9bqN78Le1dbC1g, createdTime: 2020-10-20 19:10:21.000Z, fields: {Short Description: Test Description})
I didn't look for the remaining methods, but my advice is to write your own connection to the airtime API or use the existing package and change the things that aren't working. Hopefully this helps you.
Edit:
I'll checked the repository again, and it seems that the author is inactive. As it only contains a few methods and models to work with the airtable API, I'll guess it's not a bad idea to just wrote your own implementation.
Here an simple example of how to do this (with Dio for the network connection):
try {
final response = await Dio().post(
'https://api.airtable.com/v0/$projectBase/$recordName',
options: Options(
contentType: 'Application/json',
headers: {
'Authorization': 'Bearer $yourApiKey',
'Accept': 'Application/json',
},
),
data: {
'records': [
{
'fields': {
'Short Description': 'Cactus',
'Total': 11.5,
}
},
],
},
);
// TODO: Whatever you want to do with the response. A good practice is to transform it into models and than work with them
print(response);
} on DioError catch (e) {
// TODO: Error handling
if (e.response != null) {
print(e.response.data);
} else {
print(e.request);
print(e.message);
}
}
Response:
{"records":[{"id":"recrvxH93gJgAGo7j","fields":{"Short Description":"Cactus","Total":11.5},"createdTime":"2020-10-21T20:41:19.000Z"}]}
You could now extend this, maybe as a service with the GetIt package and add your required funtions. I definetly recommend to also use models for your response (have a look at this).
I used one of their example workspaces. The projectBase was an ID. You can just look at their API documentation to see how to build the requests.
Edit 2:
Read could be simple done by this:
final response = await Dio().get(
'https://api.airtable.com/v0/$projectBase/$recordName',
options: Options(
contentType: 'Application/json',
headers: {
'Authorization': 'Bearer $yourApiKey',
'Accept': 'Application/json',
},
),
);
Update:
final response = await Dio().patch(
'https://api.airtable.com/v0/$projectBase/$recordName',
options: Options(
contentType: 'Application/json',
headers: {
'Authorization': 'Bearer $yourApiKey',
'Accept': 'Application/json',
},
),
data: {
'records': [
{
'id': 'rechYkD0pDW1NjFAF',
'fields': {
'Short Description': 'Cactus II',
'Total': 11.5,
}
},
],
},
);
And delete:
final response = await Dio().delete(
'https://api.airtable.com/v0/$projectBase/$recordName/rec0bMv507juqS7pv',
options: Options(
headers: {
'Authorization': 'Bearer $yourApiKey',
'Accept': 'Application/json',
},
),
);
These are only examples, I advice you to also read the API documentation to get a overview of what is possible. And your own implementation with proper error handling.
Edit 3:
You can use the filterByFormula parameter to retrieve all the data that matches your condition (Reference).
E.g.:
final response = await Dio().get(
'https://api.airtable.com/v0/$projectBase/$recordName',
queryParameters: {
'filterByFormula': 'SEARCH("Cactus",{Short Description})' // Searches the value 'Cactus' in the 'Short description' field.
},
options: Options(
headers: {
'Authorization': 'Bearer $yourApiKey',
'Accept': 'Application/json',
},
),
);

How to remove Authorization header on redirect on any Flutter/Dart http client

I'm currently working on a project which like a lot of other projects works with s3 storage. In this case the storage is linked via the back-end.
The situation is like this, I can get the 'attachment' via an URL, lets say example.com/api/attachments/{uuid}. If the user is authorized (via the header Authorization) it should return a 302 statuscode and redirect to the s3 url. The problem is that after the redirect the Authorization header persists and the http client return a 400 response and it's because of the persisting Authorization header. Is there any way I can remove the Authorization header after redirect without catching the first request and firing a new one?
My http client code currently looks like this:
#override
Future get({
String url,
Map<String, dynamic> data,
Map<String, String> parameters,
}) async {
await _refreshClient();
try {
final response = await dio.get(
url,
data: json.encode(data),
queryParameters: parameters,
);
return response.data;
} on DioError catch (e) {
throw ServerException(
statusCode: e.response.statusCode,
message: e.response.statusMessage,
);
}
}
Future<void> _refreshClient() async {
final token = await auth.token;
dio.options.baseUrl = config.baseUrl;
dio.options.headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
});
dio.options.contentType = 'application/json';
}
Good news! This has been fixed recently with Dart 2.16 / Flutter v2.10!
Related bugs in dart issue tracker:
https://github.com/dart-lang/sdk/issues/47246
https://github.com/dart-lang/sdk/issues/45410
Official announcement:
https://medium.com/dartlang/dart-2-16-improved-tooling-and-platform-handling-dd87abd6bad1
TLDR: upgrade to Flutter v2.10!
Looking at the Dio docs, it seems like this is intentional behaviour.
All headers added to the request will be added to the redirection request(s). However, any body send with the request will not be part of the redirection request(s).
https://api.flutter.dev/flutter/dart-io/HttpClientRequest/followRedirects.html
However, I understand (and agree!) that this is generally undesirable behaviour. My solution is to manually follow the redirects myself, which is not very nice but works in a pinch.
Response<String> response;
try {
response = await dio.get(
url,
options: Options(
// Your headers here, which might be your auth headers
headers: ...,
// This is the key - avoid following redirects automatically and handle it ourselves
followRedirects: false,
),
);
} on DioError catch (e) {
final initialResponse = e.response;
// You can modify this to understand other kinds of redirects like 301 or 307
if (initialResponse != null && initialResponse.statusCode == 302) {
response = await dio.get(
initialResponse.headers.value("location")!, // We must get a location header if we got a redirect
),
);
} else {
// Rethrow here in all other cases
throw e;
}
}

Unable to get Moxios stubRequest to work

I'm having issues getting stubRequest to work properly. Here's my code:
it('should stub my request', (done) => {
moxios.stubRequest('/authenticate', {
status: 200
})
//here a call to /authenticate is being made
SessionService.login('foo', 'bar')
moxios.wait(() => {
expect(something).toHaveHappened()
done()
})
})
This works fine:
it('should stub my request', (done) => {
SessionService.login('foo', 'bar')
moxios.wait(async () => {
let request = moxios.requests.mostRecent()
await request.respondWith({
status: 200
})
expect(something).toHaveHappened()
done()
})
})
The second method just get's the last call though, and I'd really like to be able to explicitely stub certain requests.
I'm running Jest with Vue.
I landed here with a similar goal and eventually solved it using a different approach that may be helpful to others:
moxios.requests has a method .get() (source code) that lets you grab a specific request from moxios.requests based on the url. This way, if you have multiple requests, your tests don't require the requests to occur in a specific order to work.
Here's what it looks like:
moxios.wait(() => {
// Grab a specific API request based on the URL
const request = moxios.requests.get('get', 'endpoint/to/stub');
// Stub the response with whatever you would like
request.respondWith(yourStubbedResponseHere)
.then(() => {
// Your assertions go here
done();
});
});
NOTE:
The name of the method .get() is a bit misleading. It can handle different types of HTTP requests. The type is passed as the first parameter like: moxios.requests.get(requestType, url)
it would be nice if you show us the service. Service call must be inside the moxios wait func and outside must be the axios call alone. I have pasted a simplified with stubRequest
describe('Fetch a product action', () => {
let onFulfilled;
let onRejected;
beforeEach(() => {
moxios.install();
store = mockStore({});
onFulfilled = sinon.spy();
onRejected = sinon.spy();
});
afterEach(() => {
moxios.uninstall();
});
it('can fetch the product successfully', done => {
const API_URL = `http://localhost:3000/products/`;
moxios.stubRequest(API_URL, {
status: 200,
response: mockDataSingleProduct
});
axios.get(API_URL, mockDataSingleProduct).then(onFulfilled);
const expectedActions = [
{
type: ACTION.FETCH_PRODUCT,
payload: mockDataSingleProduct
}
];
moxios.wait(function() {
const response = onFulfilled.getCall(0).args[0];
expect(onFulfilled.calledOnce).toBe(true);
expect(response.status).toBe(200);
expect(response.data).toEqual(mockDataSingleProduct);
return store.dispatch(fetchProduct(mockDataSingleProduct.id))
.then(() => {
var actions = store.getActions();
expect(actions.length).toBe(1);
expect(actions[0].type).toBe(ACTION.FETCH_PRODUCT);
expect(actions[0].payload).not.toBe(null || undefined);
expect(actions[0].payload).toEqual(mockDataSingleProduct);
expect(actions).toEqual(expectedActions);
done();
});
});
});
})

How to download files using axios

I am using axios for basic http requests like GET and POST, and it works well. Now I need to be able to download Excel files too. Is this possible with axios? If so does anyone have some sample code? If not, what else can I use in a React application to do the same?
Download the file with Axios as a responseType: 'blob'
Create a file link using the blob in the response from Axios/Server
Create <a> HTML element with a the href linked to the file link created in step 2 & click the link
Clean up the dynamically created file link and HTML element
axios({
url: 'http://api.dev/file-download', //your url
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
// create file link in browser's memory
const href = URL.createObjectURL(response.data);
// create "a" HTML element with href to file & click
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'file.pdf'); //or any other extension
document.body.appendChild(link);
link.click();
// clean up "a" element & remove ObjectURL
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
Check out the quirks at https://gist.github.com/javilobo8/097c30a233786be52070986d8cdb1743
Full credits to: https://gist.github.com/javilobo8
More documentation for URL.createObjectURL is available on MDN. It's critical to release the object with URL.revokeObjectURL to prevent a memory leak. In the function above, since we've already downloaded the file, we can immediately revoke the object.
Each time you call createObjectURL(), a new object URL is created, even if you've already created one for the same object. Each of these must be released by calling URL.revokeObjectURL() when you no longer need them.
Browsers will release object URLs automatically when the document is unloaded; however, for optimal performance and memory usage, if there are safe times when you can explicitly unload them, you should do so.
When response comes with a downloadable file, response headers will be something like
Content-Disposition: "attachment;filename=report.xls"
Content-Type: "application/octet-stream" // or Content-type: "application/vnd.ms-excel"
What you can do is create a separate component, which will contain a hidden iframe.
import * as React from 'react';
var MyIframe = React.createClass({
render: function() {
return (
<div style={{display: 'none'}}>
<iframe src={this.props.iframeSrc} />
</div>
);
}
});
Now, you can pass the url of the downloadable file as prop to this component, So when this component will receive prop, it will re-render and file will be downloaded.
Edit: You can also use js-file-download module. Link to Github repo
const FileDownload = require('js-file-download');
Axios({
url: 'http://localhost/downloadFile',
method: 'GET',
responseType: 'blob', // Important
}).then((response) => {
FileDownload(response.data, 'report.csv');
});
Downloading Files (using Axios and Security)
This is actually even more complex when you want to download files using Axios and some means of security. To prevent anyone else from spending too much time in figuring this out, let me walk you through this.
You need to do 3 things:
Configure your server to permit the browser to see required HTTP headers
Implement the server-side service, and making it advertise the correct file type for the downloaded file.
Implementing an Axios handler to trigger a FileDownload dialog within the browser
These steps are mostly doable - but are complicated considerably by the browser's relation to CORS. One step at a time:
1. Configure your (HTTP) server
When employing transport security, JavaScript executing within a browser can [by design] access only 6 of the HTTP headers actually sent by the HTTP server. If we would like the server to suggest a filename for the download, we must inform the browser that it is "OK" for JavaScript to be granted access to other headers where the suggested filename would be transported.
Let us assume - for the sake of discussion - that we want the server to transmit the suggested filename within an HTTP header called X-Suggested-Filename. The HTTP server tells the browser that it is OK to expose this received custom header to the JavaScript/Axios with the following header:
Access-Control-Expose-Headers: X-Suggested-Filename
The exact way to configure your HTTP server to set this header varies from product to product.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers for a full explanation and detailed description of these standard headers.
2. Implement the server-side service
Your server-side service implementation must now perform 2 things:
Create the (binary) document and assign the correct ContentType to the response
Assign the custom header (X-Suggested-Filename) containing the suggested file name for the client
This is done in different ways depending on your chosen technology stack. I will sketch an example using the JavaEE 7 standard which should emit an Excel report:
#GET
#Path("/report/excel")
#Produces("application/vnd.ms-excel")
public Response getAllergyAndPreferencesReport() {
// Create the document which should be downloaded
final byte[] theDocumentData = ....
// Define a suggested filename
final String filename = ...
// Create the JAXRS response
// Don't forget to include the filename in 2 HTTP headers:
//
// a) The standard 'Content-Disposition' one, and
// b) The custom 'X-Suggested-Filename'
//
final Response.ResponseBuilder builder = Response.ok(
theDocumentData, "application/vnd.ms-excel")
.header("X-Suggested-Filename", fileName);
builder.header("Content-Disposition", "attachment; filename=" + fileName);
// All Done.
return builder.build();
}
The service now emits the binary document (an Excel report, in this case), sets the correct content type - and also sends a custom HTTP header containing the suggested filename to use when saving the document.
3. Implement an Axios handler for the Received document
There are a few pitfalls here, so let's ensure all details are correctly configured:
The service responds to #GET (i.e. HTTP GET), so the Axios call must be 'axios.get(...)'.
The document is transmitted as a stream of bytes, so you must tell Axios to treat the response as an HTML5 Blob. (I.e. responseType: 'blob').
In this case, the file-saver JavaScript library is used to pop the browser dialog open. However, you could choose another.
The skeleton Axios implementation would then be something along the lines of:
// Fetch the dynamically generated excel document from the server.
axios.get(resource, {responseType: 'blob'}).then((response) => {
// Log somewhat to show that the browser actually exposes the custom HTTP header
const fileNameHeader = "x-suggested-filename";
const suggestedFileName = response.headers[fileNameHeader];
const effectiveFileName = (suggestedFileName === undefined
? "allergierOchPreferenser.xls"
: suggestedFileName);
console.log(`Received header [${fileNameHeader}]: ${suggestedFileName}, effective fileName: ${effectiveFileName}`);
// Let the user save the file.
FileSaver.saveAs(response.data, effectiveFileName);
}).catch((response) => {
console.error("Could not Download the Excel report from the backend.", response);
});
Axios.post solution with IE and other browsers
I've found some incredible solutions here. But they frequently don't take into account problems with IE browser. Maybe it will save some time to somebody else.
axios.post("/yourUrl",
data,
{ responseType: 'blob' }
).then(function (response) {
let fileName = response.headers["content-disposition"].split("filename=")[1];
if (window.navigator && window.navigator.msSaveOrOpenBlob) { // IE variant
window.navigator.msSaveOrOpenBlob(new Blob([response.data],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
),
fileName
);
} else {
const url = window.URL.createObjectURL(new Blob([response.data],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download',
response.headers["content-disposition"].split("filename=")[1]);
document.body.appendChild(link);
link.click();
}
}
);
example above is for excel files, but with little changes can be applied to any format.
And on server I've done this to send an excel file.
response.contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=exceptions.xlsx")
The function to make the API call with axios:
function getFileToDownload (apiUrl) {
return axios.get(apiUrl, {
responseType: 'arraybuffer',
headers: {
'Content-Type': 'application/json'
}
})
}
Call the function and then download the excel file you get:
getFileToDownload('putApiUrlHere')
.then (response => {
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type, encoding: 'UTF-8' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'file.xlsx'
link.click()
})
It's very simple javascript code to trigger a download for the user:
window.open("<insert URL here>")
You don't want/need axios for this operation; it should be standard to just let the browser do it's thing.
Note: If you need authorisation for the download then this might not work. I'm pretty sure you can use cookies to authorise a request like this, provided it's within the same domain, but regardless, this might not work immediately in such a case.
As for whether it's possible... not with the in-built file downloading mechanism, no.
axios.get(
'/app/export'
).then(response => {
const url = window.URL.createObjectURL(new Blob([response]));
const link = document.createElement('a');
link.href = url;
const fileName = `${+ new Date()}.csv`// whatever your file name .
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();// you need to remove that elelment which is created before.
})
The trick is to make an invisible anchor tag in the render() and add a React ref allowing to trigger a click once we have the axios response:
class Example extends Component {
state = {
ref: React.createRef()
}
exportCSV = () => {
axios.get(
'/app/export'
).then(response => {
let blob = new Blob([response.data], {type: 'application/octet-stream'})
let ref = this.state.ref
ref.current.href = URL.createObjectURL(blob)
ref.current.download = 'data.csv'
ref.current.click()
})
}
render(){
return(
<div>
<a style={{display: 'none'}} href='empty' ref={this.state.ref}>ref</a>
<button onClick={this.exportCSV}>Export CSV</button>
</div>
)
}
}
Here is the documentation: https://reactjs.org/docs/refs-and-the-dom.html. You can find a similar idea here: https://thewebtier.com/snippets/download-files-with-axios/.
There are a couple of critical points most of the answers are missing.
I will try to explain in much depth here.
TLDR;
If you are creating an a tag link and initiating a download through broswer request, then
Always call window.URL.revokeObjectURL(url);. Else there can be
unnecessary memory spikes.
There is NO need to append the created link to the document body using document.body.appendChild(link);, preventing the unnecessary need to remove the child later.
For Component code and a deeper analysis, read further
First is to figure out if the API endpoint from which you are trying to download the data is public or private. Do you have control over the server or not?
If the server responds with
Content-Disposition: attachment; filename=dummy.pdf
Content-Type: application/pdf
Browser will always try to download the file with the name 'dummy.pdf'
If the server responds with
Content-Disposition: inline; filename=dummy.pdf
Content-Type: application/pdf
Browser will first try to open a native file reader if available with the name 'dummy.pdf', else it will start file download.
If the server responds with neither of the above 2 headers
Browser (atleast chrome) will try to open the file if the download attribute is not set. If set, it will download the file. The name of the file will be the value of the last path param in cases where the url is not a blob.
Apart from that keep in mind to use Transfer-Encoding: chunked from server to transfer large volumes of data from the server. This will ensure the client knows when to stop reading from the current request in the absence of Content-Length header
For Private Files
import { useState, useEffect } from "react";
import axios from "axios";
export default function DownloadPrivateFile(props) {
const [download, setDownload] = useState(false);
useEffect(() => {
async function downloadApi() {
try {
// It doesn't matter whether this api responds with the Content-Disposition header or not
const response = await axios.get(
"http://localhost:9000/api/v1/service/email/attachment/1mbdoc.docx",
{
responseType: "blob", // this is important!
headers: { Authorization: "sometoken" },
}
);
const url = window.URL.createObjectURL(new Blob([response.data])); // you can mention a type if you wish
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "dummy.docx"); //this is the name with which the file will be downloaded
link.click();
// no need to append link as child to body.
setTimeout(() => window.URL.revokeObjectURL(url), 0); // this is important too, otherwise we will be unnecessarily spiking memory!
setDownload(false);
} catch (e) {} //error handling }
}
if (download) {
downloadApi();
}
}, [download]);
return <button onClick={() => setDownload(true)}>Download Private</button>;
}
For Public Files
import { useState, useEffect } from "react";
export default function DownloadPublicFile(props) {
const [download, setDownload] = useState(false);
useEffect(() => {
if (download) {
const link = document.createElement("a");
link.href =
"http://localhost:9000/api/v1/service/email/attachment/dummy.pdf";
link.setAttribute("download", "dummy.pdf");
link.click();
setDownload(false);
}
}, [download]);
return <button onClick={() => setDownload(true)}>Download Public</button>;
}
Good to know:
Always control file downloads from server.
Axios in the browser uses XHR under the hood, in which streaming of responses
is not supported.
Use onDownloadProgress method from Axios to implement progress bar.
Chunked responses from server do not ( cannot ) indicate Content-Length. Hence you need some way of knowing the response size if you are using them while building a progress bar.
<a> tag links can only make GET HTTP requests without any ability to send headers or
cookies to the server (ideal for downloading from public endpoints)
Brower request is slightly different from XHR request made in code.
Ref: Difference between AJAX request and a regular browser request
File download with custom header request. In this example, it shows how to send file download request with the bearer token. Good for downloadable content with authorization.
download(urlHere) {
axios.get(urlHere, {
headers: {
"Access-Control-Allow-Origin": "*",
Authorization: `Bearer ${sessionStorage.getItem("auth-token")}`,
}
}).then((response) => {
const temp = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = temp;
link.setAttribute('download', 'file.csv'); //or any other extension
document.body.appendChild(link);
link.click();
});
}
You need to return File({file_to_download}, "application/vnd.ms-excel") from your backend to the frontend and in your js file you need to update the code that is written below:
function exportToExcel() {
axios.post({path to call your controller}, null,
{
headers:
{
'Content-Disposition': "attachment; filename=XYZ.xlsx",
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
},
responseType: 'arraybuffer',
}
).then((r) => {
const path= window.URL.createObjectURL(new Blob([r.data]));
const link = document.createElement('a');
link.href = path;
link.setAttribute('download', 'XYZ.xlsx');
document.body.appendChild(link);
link.click();
}).catch((error) => console.log(error));
}
For those who'd like to implement an authenticated native download.
I'm currently developing a SPA with Axios.
Unfortunately Axios does't allow stream response type in such case.
From documentation:
// `responseType` indicates the type of data that the server will respond with
// options are: 'arraybuffer', 'document', 'json', 'text', 'stream'
// browser only: 'blob'
But I figured out a workaround as mentioned in this topic.
The trick is to send a basic Form POST containing your token and the targeted file.
"That targets a new window. Once the browser reads the attachment header on the server response, it will close the new tab and begin the download."
Here's a sample:
let form = document.createElement('form');
form.method = 'post';
form.target = '_blank';
form.action = `${API_URL}/${targetedResource}`;
form.innerHTML = `'<input type="hidden" name="jwtToken" value="${jwtToken}">'`;
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
"You may need to mark your handler as unauthenticated/anonymous so that you can manually validate the JWT to ensure proper authorization."
Which results for my ASP.NET implementation in:
[AllowAnonymous]
[HttpPost("{targetedResource}")]
public async Task<IActionResult> GetFile(string targetedResource, [FromForm] string jwtToken)
{
var jsonWebTokenHandler = new JsonWebTokenHandler();
var validationParameters = new TokenValidationParameters()
{
// Your token validation parameters here
};
var tokenValidationResult = jsonWebTokenHandler.ValidateToken(jwtToken, validationParameters);
if (!tokenValidationResult.IsValid)
{
return Unauthorized();
}
// Your file upload implementation here
}
This Worked for me. i implemented this solution in reactJS
const requestOptions = {`enter code here`
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
fetch(`${url}`, requestOptions)
.then((res) => {
return res.blob();
})
.then((blob) => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'config.json'); //or any other extension
document.body.appendChild(link);
link.click();
})
.catch((err) => {
return Promise.reject({ Error: 'Something Went Wrong', err });
})
I had an issue where transferring one file I downloaded from axios const axiosResponse = await axios.get(pdf.url) to google drive googleDrive.files.create({media: {body: axiosResponse.data, mimeType}, requestBody: {name: fileName, parents: [parentFolder], mimeType}, auth: jwtClient}) uploaded a corrupted file.
The reason the file was corrupted was because axios transformed the axiosResponse.data to a string. To solve the issue, I had to ask axios to return a stream axios.get(pdf.url, { responseType: 'stream' }).
Implement an Axios handler for the Received document, the data format octect-stream,
data might look weird PK something JbxfFGvddvbdfbVVH34365436fdkln as its octet stream format, you might end up creating file with this data might be corrupt, {responseType: 'blob'} will make data into readable format,
axios.get("URL", {responseType: 'blob'})
.then((r) => {
let fileName = r.headers['content-disposition'].split('filename=')[1];
let blob = new Blob([r.data]);
window.saveAs(blob, fileName);
}).catch(err => {
console.log(err);
});
you might have tried solution which fails like this,
window.saveAs(blob, 'file.zip') will try to save file as zip but will wont work,
const downloadFile = (fileData) => {
axios.get(baseUrl+"/file/download/"+fileData.id)
.then((response) => {
console.log(response.data);
const blob = new Blob([response.data], {type: response.headers['content-type'], encoding:'UTF-8'});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'file.zip';
link.click();
})
.catch((err) => console.log(err))
}
const downloadFile = (fileData) => {
axios.get(baseUrl+"/file/download/"+fileData.id)
.then((response) => {
console.log(response);
//const binaryString = window.atob(response.data)
//const bytes = new Uint8Array(response.data)
//const arrBuff = bytes.map((byte, i) => response.data.charCodeAt(i));
//var base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(response.data)));
const blob = new Blob([response.data], {type:"application/octet-stream"});
window.saveAs(blob, 'file.zip')
// const link = document.createElement('a');
// link.href = window.URL.createObjectURL(blob);
// link.download = 'file.zip';
// link.click();
})
.catch((err) => console.log(err))
}
function base64ToArrayBuffer(base64) {
var binaryString = window.atob(base64);
var binaryLen = binaryString.length;
var bytes = new Uint8Array(binaryLen);
for (var i = 0; i < binaryLen; i++) {
var ascii = binaryString.charCodeAt(i);
bytes[i] = ascii;
};
return bytes;
}
another short solution is,
window.open("URL")
will keep opening new tabs unnecessarily and user might have to make allow popups for work this code, what if user want to download multiple files at the same time so go with solution first or if not try for other solutions also
This function will help you to download a ready xlsx, csv etc file download. I just send a ready xlsx static file from backend and it in react.
const downloadFabricFormat = async () => {
try{
await axios({
url: '/api/fabric/fabric_excel_format/',
method: 'GET',
responseType: 'blob',
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'Fabric Excel Format.xlsx');
document.body.appendChild(link);
link.click();
});
} catch(error){
console.log(error)
}
};
Basically, I solved the problem of the filename by reading it, if present, from the 'content-disposition' header:
const generateFile = async ({ api, url, payload }) => {
return await api({
url: url,
method: 'POST',
data: payload, // payload
responseType: 'blob'
}).catch((e) => {
throw e;
});
};
const getFileName = (fileBlob, defaultFileName) => {
const contentDisposition = fileBlob.headers.get('content-disposition');
if (contentDisposition) {
const fileNameIdentifier = 'filename=';
const filenamePosition = contentDisposition.indexOf(fileNameIdentifier);
if (~filenamePosition) {
return contentDisposition.slice(filenamePosition + fileNameIdentifier.length, contentDisposition.length).replace(/"/g,'');
}
}
return defaultFileName;
};
const downloadFile = (fileBlob, fileName) => {
const url = window.URL.createObjectURL(new Blob([fileBlob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${fileName}`);
document.body.appendChild(link);
link.click();
link.remove();
link.style.display = 'none';
window.URL.revokeObjectURL(url);
};
// "api" is an instance of Axios (axios.create)
// "payload" is the payload you submit to the server
const fileBlob = await generateFile({ api, '/url/to/download', payload });
const fileName = getFileName(fileBlob, "MyDownload.xls");
downloadFile(fileBlob.data, fileName);
For axios POST request, the request should be something like this:
The key here is that the responseType and header fields must be in the 3rd parameter of Post. The 2nd parameter is the application parameters.
export const requestDownloadReport = (requestParams) => async dispatch => {
let response = null;
try {
response = await frontEndApi.post('createPdf', {
requestParams: requestParams,
},
{
responseType: 'arraybuffer', // important...because we need to convert it to a blob. If we don't specify this, response.data will be the raw data. It cannot be converted to blob directly.
headers: {
'Content-Type': 'application/json',
'Accept': 'application/pdf'
}
});
}
catch(err) {
console.log('[requestDownloadReport][ERROR]', err);
return err
}
return response;
}
The answers using URL.CreateObject() have worked well for me.
I still want to point out the option of using HTTP Headers.
Using HttpHeaders has these advantages:
very widespread browser support
does not require creating a blob object in the browser's memory
does not require waiting for the full response from the server before showing giving the user feedback
no size limitations
Using HttpHeaders requires you to have access to the back-end server where the files are downloaded from (which seems to be the case for OP's Excel files)
HttpHeaders solution:
FRONT-END:
//...
// the download link
<a href="download/destination?parameter1=foo&param2=bar">
click me to download!
</a>
BACK-END
(C# in this example, but could be any language. Adapt as required)
...
var fs = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Read);
Response.Headers["Content-Disposition"] = "attachment; filename=someName.txt";
return File(fs, "application/octet-stream");
...
This solution assumes you have control of the back-end server that responds.
https://github.com/eligrey/FileSaver.js/wiki/Saving-a-remote-file#using-http-header
My answer is a total hack- I just created a link that looks like a button and add the URL to that.
<a class="el-button"
style="color: white; background-color: #58B7FF;"
:href="<YOUR URL ENDPOINT HERE>"
:download="<FILE NAME NERE>">
<i class="fa fa-file-excel-o"></i> Excel
</a>
I'm using the excellent VueJs hence the odd anotations, however, this solution is framework agnostic. The idea would work for any HTML based design.