send_file and send_from_directory changed their parameter names in Flask 2.0. attachment_filename became download_name, cache_timeout became max_age, add_etags became etag, and filename became path. The old names were deprecated in 2.0 and removed in 2.2, so code that survived the warning phase fails with an unexpected keyword argument TypeError.
A behavioural change came with the rename: passing download_name no longer implies a download. as_attachment=True is what sets the Content-Disposition attachment header, and setting only the name gives you an inline response with a filename hint.
When serving a file-like object rather than a path, Flask cannot guess the MIME type or the name, so pass both mimetype and download_name explicitly or the browser gets application/octet-stream. For user-supplied paths use send_from_directory, which validates that the resolved path stays inside the directory; building a path yourself and passing it to send_file is the standard directory traversal bug.