reactjsfastapichatbot

how to download a file with a dynamic name using ReactJS and FastAPI?


I have created a chatbot that has a backend created with FastAPI and the following function:

@router.get("/download/")
async def download_file(filename: str, collection_name: str) -> FileResponse:
    file_path = f"data/files/{collection_name}/{filename}"
    
    headers = {'Access-Control-Expose-Headers': 'Content-Disposition'}
    
    if not os.path.exists(file_path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found.")
    return FileResponse(file_path, filename=filename, headers=headers)

Then, I created a ReactJS component that generates the message and embeds download links to it so that I can download the referenced files:

import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

function ChatMessage({ message, collection }) {
    const generateMarkdownLinks = (sources) => {
        return sources.map((source, index) => `[Download Source ${index + 1}](/download/${encodeURIComponent(source)})`).join("  \n");
    };

    const markdownContent = message.isBot && message.sources && message.sources.length > 0
        ? `${message.text}\n\n${generateMarkdownLinks(message.sources)}`
        : message.text;

    const handleDownloadClick = async (e, filepath, collectionName) => {
        e.preventDefault();
        const filename = decodeURIComponent(filepath.split('%2F').pop());
        const url = `/download/?filename=${encodeURIComponent(filename)}&collection_name=${encodeURIComponent(collectionName)}`;

        console.log(`Downloading file: ${filename}`);
        console.log(`Filepath: ${filepath}`);
        console.log(`Collection: ${collectionName}`);
        console.log(`URL: ${url}`);

        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`Network response was not ok: ${response.statusText}`);
            }
            const blob = await response.blob();
            const downloadUrl = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = downloadUrl;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            window.URL.revokeObjectURL(downloadUrl);
        } catch (error) {
            console.error('File download failed:', error);
            alert('File download failed.');
        }
    };

    const renderLink = ({ href, children }) => {
        if (href.startsWith('/download/')) {
            const filepath = href.replace('/download/', '');
            return (
                <a
                    href={href}
                    onClick={(e) => handleDownloadClick(e, filepath, collection)}
                >
                    {children}
                </a>
            );
        }
        return <a href={href}>{children}</a>;
    };

    return (
        <div className={`chat-message ${message.isBot ? 'bot-message' : 'user-message'}`}>
            {message.isBot ? (
                <ReactMarkdown
                    remarkPlugins={[remarkGfm]}
                    components={{
                        a: renderLink
                    }}
                >
                    {markdownContent}
                </ReactMarkdown>
            ) : (
                <>
                    {message.text}
                    {message.sources && message.sources.length > 0 && message.sources.map((source, index) => (
                        <a
                            key={index}
                            href={`/download/${encodeURIComponent(source)}`}
                            onClick={(e) => handleDownloadClick(e, source, message.collectionName)}
                        >
                            Download Source {index + 1}
                        </a>
                    ))}
                </>
            )}
        </div>
    );
}

export default ChatMessage;

The problem occurs when I click any of the links, I am able to download a file, but it is corrupt and doesn't contain the information from the original one. I assume, maybe wrongly, that the problem is the frontend code, because I am able to properly download the file with the backend using the Swagger documentation from FastAPI.

Could anyone help with that? Thank you very much.


Solution

  • I have figured out what the problem was. The URL in the handleDownloadClick function was missing an important part at the beginning. So, I reformatted it like this:

    const baseUrl = `${window.location.origin}/api`;
        const url = `${baseUrl}/download/?filename=${encodeURIComponent(filename)}&collection_name=${encodeURIComponent(collectionName)}`;
    

    This dynamically generates the URL based on the server address and handles the download properly.