How to Force Nginx to Serve Plaintext Files as Downloads Instead of Inline Display


3 views

When working with Nginx as a reverse proxy for applications like Redmine, you'll notice that browsers tend to display plaintext files (like .txt) inline rather than triggering a download prompt. This behavior stems from how Nginx handles Content-Disposition headers for different MIME types.

By default, Nginx uses the default_type directive and its MIME types configuration (/etc/nginx/mime.types) to determine how to serve files. Text files typically receive a text/plain Content-Type without a Content-Disposition: attachment header, which tells browsers to display the content.

Add this to your Nginx server block or location configuration:

location ~* \.(txt|log|ini)$ {
    add_header Content-Disposition "attachment";
    default_type application/octet-stream;
}

For a Redmine-specific solution handling attachments through Rails, consider this comprehensive approach:

location /attachments {
    # Force download for specific text-based formats
    if ($request_filename ~* ^.+\.(txt|log|conf|ini|csv)$) {
        add_header Content-Disposition "attachment";
    }
    
    # Standard proxy configuration
    proxy_pass http://redmine_app;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

After applying these changes:

  1. Reload Nginx: sudo nginx -s reload
  2. Clear your browser cache
  3. Test with various file types to verify the behavior

Another approach is to override the MIME type completely:

location ~* \.txt$ {
    types { }
    default_type application/octet-stream;
}

When working with file attachments in web applications, you'll notice browsers handle different MIME types differently. For text files (.txt, .csv, .log, etc.), browsers typically render the content directly in the viewport rather than triggering a download dialog. This behavior stems from the Content-Disposition header not being properly configured.

Nginx determines whether to serve files inline or as downloads based on two key headers:

1. Content-Type: Identifies the MIME type of the file
2. Content-Disposition: Controls presentation style (inline vs attachment)

Add this to your Nginx configuration (nginx.conf or site-specific config):

location ~* \.(txt|log|csv)$ {
    add_header Content-Disposition "attachment";
    types { } default_type application/octet-stream;
}

For more granular control, consider these variations:

# Force download for all text MIME types
location ~* ^/.+\.(txt|log|csv)$ {
    if ($request_filename ~ ^.*?/([^/]*?)$) {
        set $filename $1;
    }
    add_header Content-Disposition "attachment; filename=\"$filename\"";
    default_type application/octet-stream;
}

# Preserve original filename with dynamic content
location /downloads/ {
    add_header Content-Disposition "attachment";
    types {
        text/plain txt log;
        text/csv csv;
    }
}

After making changes:

sudo nginx -t       # Test configuration
sudo systemctl reload nginx

Verify with curl:

curl -I http://yoursite.com/path/to/file.txt
# Should show:
# Content-Disposition: attachment
# Content-Type: application/octet-stream

1. Caching issues: Clear browser cache after changes
2. Overriding existing rules: Ensure your new location block takes precedence
3. Proxied content: If using Nginx as reverse proxy, set headers at both levels

The application/octet-stream MIME type bypasses Nginx's default type processing. For high-traffic sites handling many small text files, consider:

location ~* \.(txt|log|csv)$ {
    add_header Content-Disposition "attachment";
    types {
        text/plain txt log;
        text/csv csv;
    }
    gzip off;  # Disable compression for these files
}