Serving data files
TL;DR: JBrowse reads your data files directly over HTTP with byte-range
requests, so the server has to return the raw bytes of a range rather than a
whole, re-encoded file. The two settings that break that are
Content-Encoding: gzip applied to BGZF files and a missing CORS policy on a
separate data host.
What the server has to support
JBrowse 2 is static JS/CSS/HTML, no backend required. Deploy by copying the
folder to your web server (e.g. /var/www/html/) or Amazon S3.
The server must support byte-range requests (the
Range HTTP header)
so that JBrowse can get small slices of large binary data files. A server that
ignores Range and returns whole files turns every read into a full download.
If you use Django, put jbrowse-web in the static resources folder, but serve data files from a separate server (Django's static resources folder won't serve them correctly). For some informal troubleshooting notes, see these notes.
Configure gzip for text, never for BGZF
JBrowse Web is roughly 2MB of JavaScript, which gzip cuts to about a third of
that, and the same setting shrinks config.json (a config with hundreds of
tracks is mostly repeated JSON keys). Most cloud hosts, including AWS
CloudFront, Amplify and Netlify, compress text responses automatically. Apache
and Nginx have to be told.
For Nginx, add to your server block:
gzip on;
gzip_types application/json text/plain text/html text/css text/javascript application/javascript;
For Apache, enable mod_deflate:
sudo a2enmod deflate
sudo systemctl restart apache2
Then add to your Apache config (e.g.
/etc/apache2/sites-available/000-default.conf):
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
This applies to text only. Never gzip BGZF binary files, see the next section.
Indexed binary files do not work on my server
Almost always: the server is sending Content-Encoding: gzip on a
BGZF-compressed file (BAM, VCF.gz, GFF.gz, BED.gz, .fa.gz, etc.).
BGZF looks like gzip to the server, so content sniffers - Apache's
mod_mime_magic, PHP's mime_content_type, some CDN auto-rules - add the
header, and the browser then decompresses the file before JavaScript sees it.
JBrowse needs the raw bytes: it does its own BGZF decompression and seeks into
the file using offsets from .bai/.tbi/.csi/.gzi. What you get instead is
truncated data, "invalid BGZF block", or random gaps, and byte range requests
break the same way.
The fix: don't set Content-Encoding on these files. Serve them as opaque
binary.
-
On Apache, disable
mod_mime_magic, or scope it. To keep it on elsewhere, unset the header for genomic extensions:<FilesMatch "\.(bam|bai|cram|crai|vcf\.gz|tbi|csi|gff\.gz|bed\.gz|fa\.gz|gzi|fai)$"> Header unset Content-Encoding </FilesMatch> -
On Nginx, only
gziptext MIME types. The defaultgzip_typesis fine, just don't addapplication/octet-streamorapplication/gzip, and don't enablegzip_staticfor genomic files. -
On S3 / CloudFront, don't upload with
--content-encoding gzip. Fix a bad upload withaws s3 cp --content-encoding "" .... -
On PHP / app servers, disable auto-content-type middleware on these paths.
To check, open dev tools' Network tab, request the file, and confirm no
Content-Encoding: gzip header on the response.
The rule covers BGZF binary files only. Compressing config.json is fine.
CORS errors on remote files
A CORS error means JBrowse is served from a different domain than your data (e.g. JBrowse on one host, data on a separate S3 / MinIO bucket). JBrowse cannot work around CORS restrictions. The fix must be on the data server.
At minimum the data server must:
- return
Access-Control-Allow-Originmatching your JBrowse origin (or*), - allow the
Rangerequest header (Access-Control-Allow-Headers: Range), and - honor byte-range requests: respond
206 Partial Contentwith the requested bytes (not200with the whole file).
You do not need to expose Content-Range: JBrowse detects end-of-file from
short/416 range responses, so range reads work even when CORS hides it.
Exposing it is optional polish, letting JBrowse report the true file size in a
few places like the spreadsheet importer. Content-Length is CORS-safelisted
and always readable, so download progress works either way.
For local development only, launching Chrome with --disable-web-security is a
temporary workaround.
S3 / MinIO CORS configuration
Apply this CORS policy to the bucket (S3 console → bucket → Permissions →
Cross-origin resource sharing, or the CLI below). Replace the origin with your
JBrowse host, or use ["*"] for public data:
[
{
"AllowedOrigins": ["https://your-jbrowse-host.example.com"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["Range"],
"ExposeHeaders": ["Content-Range", "Content-Length", "Accept-Ranges"]
}
]
One-liner to apply it with the AWS CLI:
aws s3api put-bucket-cors --bucket YOUR_BUCKET --cors-configuration \
'{"CORSRules":[{"AllowedOrigins":["*"],"AllowedMethods":["GET","HEAD"],"AllowedHeaders":["Range"],"ExposeHeaders":["Content-Range","Content-Length","Accept-Ranges"]}]}'
To verify, open dev tools' Network tab and confirm the file request returns
206 Partial Content with an Access-Control-Allow-Origin header.
For MinIO, per-bucket CORS (mc cors set / the put-bucket-cors S3 API) is
only available in MinIO AIStor (the commercial edition). The community server
instead controls CORS globally with the MINIO_API_CORS_ALLOW_ORIGIN
environment variable, a comma-separated origin list that defaults to * (all
origins). Set it to your JBrowse origin and restart the server:
export MINIO_API_CORS_ALLOW_ORIGIN="https://your-jbrowse-host.example.com"