@brc-dd saved me on this issue. One of the things that I had to add was a dynamically generated link element (see var link
in the code) that clicks itself once we have the file data from the API. This was very important to get consistent downloads (which I was not getting prior).
My page code that creates the link to download the file looks like:
// the fileProps variable used below looks like {"file_name":"test.png", "file_type":"image/png", "file_size": 748833}
import Button from 'react-bootstrap/Button'
import { toast } from 'react-toastify';
const DataGridCell = ({ filename, filetype, filesize }) => {
const [objFileState, setFileDownload] = useState({})
// handle POST request here
useEffect(() => {
async function retrieveFileBlob() {
try {
const ftch = await fetch( // this will request the file information for the download (whether an image, PDF, etc.)
`/api/request-file`,
{
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(objFileState)
},
)
const fileBlob = await ftch.blob()
// this works and prompts for download
var link = document.createElement('a') // once we have the file buffer BLOB from the post request we simply need to send a GET request to retrieve the file data
link.href = window.URL.createObjectURL(fileBlob)
link.download = objFileState.strFileName
link.click()
link.remove(); //afterwards we remove the element
} catch (e) {
console.log({ "message": e, status: 400 }) // handle error
}
}
if (objFileState !== {} && objFileState.strFileId) retrieveFileBlob() // request the file from our file server
}, [objFileState])
// NOTE: it is important that the objFile is properly formatted otherwise the useEffect will just start firing off without warning
const objFile = {
"objFileProps": { "file_name": filename, "file_type": filetype, "file_size": filesize }
}
return <Button onClick={() => {toast("File download started"); setFileDownload(objFile) }} className="btn btn-primary m-2">Download {filename}</Button>
}
My local NextJs API endpoint (/api/qualtrics/retrieve-file) that the link calls looks like:
/**
* @abstract This API endpoint requests an uploaded file from a Qualtrics response
* (see Qualtrics API reference for more info:
https://api.qualtrics.com/guides/reference/singleResponses.json/paths/~1surveys~1%7BsurveyId%7D~1responses~1%7BresponseId%7D~1uploaded-files~1%7BfileId%7D/get)
* For this API endpoint the parameters we will be:
* Param 0 = Survey ID
* Param 1 = Response ID
* Param 2 = File ID
* Param 3 = Header object (properties of the file needed to return the file to the client)
*
*/
// This is a protected API route
import { getSession } from 'next-auth/client'
export default async function API(req, res) {
// parse the API query
const { params } = await req.query // NOTE: we must await the assignment of params from the request query
const session = await getSession({ req })
const strSurveyId = await params[0]
const strResponseId = await params[1]
const strFileId = await params[2]
const objFileProps = JSON.parse(decodeURIComponent(await params[3])) // file properties
// this if condition simply checks that a user is logged into the app in order to get data from this API
if (session) {
// ****** IMPORTANT: wrap your fetch to Qualtrics in a try statement to help prevent errors of headers already set **************
try {
const response = await fetch(
`${process.env.QUALTRICS_SERVER_URL}/API/v3/surveys/${strSurveyId}/responses/${strResponseId}/uploaded-files/${strFileId}`,
{
method: "get",
headers: {
"X-API-TOKEN": process.env.QUALTRICS_API_TOKEN
}
}
);
// get the file information from the external API
const resBlob = await response.blob();
const resBufferArray = await resBlob.arrayBuffer();
const resBuffer = Buffer.from(resBufferArray);
if (!response.ok) throw new Error(`unexpected response ${response.statusText}`);
// write the file to the response (should prompt user to download or open the file)
res.setHeader('Content-Type', objFileProps.file_type);
res.setHeader('Content-Length', objFileProps.file_size);
res.setHeader('Content-Disposition', `attachment; filename=${objFileProps.file_name}`);
res.write(resBuffer, 'binary');
res.end();
} catch (error) {
return res.send({ error: `You made an invalid request to download a file ${error}`, status: 400 })
}
} else {
return res.send({ error: 'You must sign in to view the protected content on this page...', status: 401 })
}
}