Schedule a demo

File uploads and downloads

REST APIs often need to handle files - returning reports for download, accepting document uploads, or fetching attachments from external systems. The key is setting the right headers so browsers and clients know they're dealing with a file rather than JSON.

This guide covers returning files to clients, processing uploads, and working with binary data.

To expose file services to clients, create a REST channel for each endpoint.

Returning file attachments

To return a downloadable file, set Content-Disposition to attachment with a filename. The browser will prompt to save rather than displaying the content.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class DownloadReport(Service):

    name = 'reports.download'
    input = 'report_id'

    def handle(self):

        report_id = self.request.input.report_id

        # Get report content from storage
        content = self.invoke('reports.generate', report_id=report_id)

        # Return as downloadable attachment
        self.response.payload = content
        self.response.content_type = 'application/pdf'

        filename = f'report-{report_id}.pdf'
        self.response.headers['Content-Disposition'] = f'attachment; filename={filename}'

Test with curl - the -O flag saves to a file:

# Download and save as the suggested filename
curl -O -J http://localhost:11223/api/reports/download?report_id=RPT-001

# Or just see what comes back
curl http://localhost:11223/api/reports/download?report_id=RPT-001 --output report.pdf

Returning different file types

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class DownloadCSV(Service):

    name = 'export.csv'

    def handle(self):

        # Generate CSV content
        lines = [
            'id,name,email',
            '1,John,john@example.com',
            '2,Jane,jane@example.com',
        ]
        csv_data = '\n'.join(lines)

        self.response.payload = csv_data
        self.response.headers['Content-Disposition'] = 'attachment; filename=export.csv'
        self.response.content_type = 'text/csv'


class DownloadExcel(Service):

    name = 'export.excel'

    def handle(self):

        # Binary content from a library like openpyxl
        excel_bytes = self.generate_excel()

        self.response.payload = excel_bytes
        self.response.headers['Content-Disposition'] = 'attachment; filename=export.xlsx'

        # Standard MIME type for .xlsx files
        xlsx_mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        self.response.content_type = xlsx_mime

    def generate_excel(self):
        # Your Excel generation logic here
        pass

Processing file uploads

Access uploaded files from multipart form data:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class UploadDocument(Service):

    name = 'documents.upload'

    def handle(self):

        # Get form data including files
        form_data = self.request.http.get_form_data()

        # Access uploaded file
        uploaded_file = form_data.get('file')

        if uploaded_file:
            filename = uploaded_file.filename
            content = uploaded_file.read()
            content_type = uploaded_file.content_type

            # Store the file
            self.invoke('storage.save',
                filename=filename,
                content=content,
                content_type=content_type)

            self.response.payload = {
                'status': 'uploaded',
                'filename': filename,
                'size': len(content)
            }
        else:
            self.response.status_code = HTTPStatus.BAD_REQUEST
            self.response.payload = {'error': 'No file provided'}

Downloading files from external APIs

Fetch attachments from external systems:

# -*- coding: utf-8 -*-

# stdlib
from http import HTTPStatus

# requests
import requests

# Zato
from zato.server.service import Service

class FetchJiraAttachment(Service):

    name = 'jira.attachment.fetch'
    input = 'attachment_id'

    def handle(self):

        attachment_id = self.request.input.attachment_id

        # Jira returns a redirect to the actual file
        base_url = 'https://example.atlassian.net'
        url = f'{base_url}/rest/api/3/attachment/content/{attachment_id}'

        auth = (self.config.jira.username, self.config.jira.api_token)

        # First request gets redirect location
        response = requests.get(url, auth=auth, allow_redirects=False)

        if response.status_code == HTTPStatus.SEE_OTHER:
            # Follow redirect to get actual file
            file_url = response.headers['Location']
            file_response = requests.get(file_url)

            self.response.payload = {
                'content': file_response.content,
                'size': len(file_response.content)
            }

        else:
            self.response.status_code = response.status_code
            self.response.payload = {'error': 'Failed to fetch attachment'}

Base64 encoding for JSON responses

When returning binary data in JSON:

# -*- coding: utf-8 -*-

# stdlib
import base64

# Zato
from zato.server.service import Service

class GetFileAsBase64(Service):

    name = 'files.get-base64'
    input = 'file_id'

    def handle(self):

        file_id = self.request.input.file_id

        # Get binary content
        content = self.invoke('storage.get', file_id=file_id)

        # Encode as base64 for JSON transport
        encoded = base64.b64encode(content).decode('utf-8')

        self.response.payload = {
            'file_id': file_id,
            'content_base64': encoded,
            'size': len(content)
        }

Learn more