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.
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
# -*- 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
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'}
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'}
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)
}