diff --git a/app/models/blob.rb b/app/models/blob.rb
index 3869e79ba75ad42d7af6703ef3b4b764c62a43cb..954d4e4d779c95fec71d61248293032dad720b4a 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -191,9 +191,12 @@ class Blob < SimpleDelegator
     rendered_as_text? && rich_viewer
   end
 
+  def expanded?
+    !!@expanded
+  end
+
   def expand!
-    simple_viewer&.expanded = true
-    rich_viewer&.expanded = true
+    @expanded = true
   end
 
   private
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index d2aa33d994e11f80c7ca4f0f02f1b2fb1776a73d..35965d01692d28305f4c67ecb30a639372e146de 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -6,15 +6,15 @@ module BlobViewer
 
     self.loading_partial_name = 'loading'
 
-    delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class
+    delegate :partial_path, :loading_partial_path, :rich?, :simple?, :load_async?, :text?, :binary?, to: :class
 
     attr_reader :blob
-    attr_accessor :expanded
 
     delegate :project, to: :blob
 
     def initialize(blob)
       @blob = blob
+      @initially_binary = blob.binary?
     end
 
     def self.partial_path
@@ -57,14 +57,10 @@ module BlobViewer
       false
     end
 
-    def load_async?
-      self.class.load_async? && render_error.nil?
-    end
-
     def collapsed?
       return @collapsed if defined?(@collapsed)
 
-      @collapsed = !expanded && collapse_limit && blob.raw_size > collapse_limit
+      @collapsed = !blob.expanded? && collapse_limit && blob.raw_size > collapse_limit
     end
 
     def too_large?
@@ -73,6 +69,10 @@ module BlobViewer
       @too_large = size_limit && blob.raw_size > size_limit
     end
 
+    def binary_detected_after_load?
+      !@initially_binary && blob.binary?
+    end
+
     # This method is used on the server side to check whether we can attempt to
     # render the blob at all. Human-readable error messages are found in the
     # `BlobHelper#blob_render_error_reason` helper.
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 4252f27d0078bd9694685cdeb9ae175af3398ec2..013f1c267c8230dd2c4a25bd659dce57aadc8adb 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -1,13 +1,19 @@
 - hidden = local_assigns.fetch(:hidden, false)
 - render_error = viewer.render_error
-- load_async = local_assigns.fetch(:load_async, viewer.load_async?)
+- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
 
 - viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
 .blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
-  - if load_async
-    = render viewer.loading_partial_path, viewer: viewer
-  - elsif render_error
+  - if render_error
     = render 'projects/blob/render_error', viewer: viewer
+  - elsif load_async
+    = render viewer.loading_partial_path, viewer: viewer
   - else
     - viewer.prepare!
+
+    -# In the rare case where the first kilobyte of the file looks like text,
+    -# but the file turns out to actually be binary after loading all data,
+    -# we fall back on the binary Download viewer.
+    - viewer = BlobViewer::Download.new(viewer.blob) if viewer.binary_detected_after_load?
+
     = render viewer.partial_path, viewer: viewer
diff --git a/changelogs/unreleased/dm-blob-binaryness-change.yml b/changelogs/unreleased/dm-blob-binaryness-change.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f3e3af26f12205363169dd956c2f80e66c0b96c6
--- /dev/null
+++ b/changelogs/unreleased/dm-blob-binaryness-change.yml
@@ -0,0 +1,5 @@
+---
+title: Detect if file that appears to be text in the first 1024 bytes is actually
+  binary afer loading all data
+merge_request:
+author:
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index d60e607b02bd50b571706834b96fef1a662cce36..33a7624e3034b0e9925ca0e1511a4cd48e8d5bb4 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -123,6 +123,7 @@ module Gitlab
         @loaded_all_data = true
         @data = repository.lookup(id).content
         @loaded_size = @data.bytesize
+        @binary = nil
       end
 
       def name
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 82cfbfda1571e425f960bc1791c07d34c4df2a1c..45fdb36e5063453169dbb33782f08703857325a8 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
 feature 'File blob', :js, feature: true do
   let(:project) { create(:project, :public) }
 
-  def visit_blob(path, fragment = nil)
-    visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment)
+  def visit_blob(path, anchor: nil, ref: 'master')
+    visit namespace_project_blob_path(project.namespace, project, File.join(ref, path), anchor: anchor)
 
     wait_for_requests
   end
@@ -101,7 +101,7 @@ feature 'File blob', :js, feature: true do
 
     context 'visiting with a line number anchor' do
       before do
-        visit_blob('files/markdown/ruby-style-guide.md', 'L1')
+        visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1')
       end
 
       it 'displays the blob using the simple viewer' do
@@ -352,6 +352,37 @@ feature 'File blob', :js, feature: true do
     end
   end
 
+  context 'binary file that appears to be text in the first 1024 bytes' do
+    before do
+      visit_blob('encoding/binary-1.bin', ref: 'binary-encoding')
+    end
+
+    it 'displays the blob' do
+      aggregate_failures do
+        # shows a download link
+        expect(page).to have_link('Download (23.8 KB)')
+
+        # does not show a viewer switcher
+        expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+        # The specs below verify an arguably incorrect result, but since we only
+        # learn that the file is not actually text once the text viewer content
+        # is loaded asynchronously, there is no straightforward way to get these
+        # synchronously loaded elements to display correctly.
+        #
+        # Clicking the copy button will result in nothing being copied.
+        # Clicking the raw button will result in the binary file being downloaded,
+        # as expected.
+
+        # shows an enabled copy button, incorrectly
+        expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+        # shows a raw button, incorrectly
+        expect(page).to have_link('Open raw')
+      end
+    end
+  end
+
   context '.gitlab-ci.yml' do
     before do
       project.add_master(project.creator)
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index d56379eb59dc43499c350fd6905bfa3a4fdc6119..574438838d80fd30a2731b6d4f89d0e2dbb9ce35 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -106,9 +106,9 @@ describe BlobViewer::Base, model: true do
   end
 
   describe '#render_error' do
-    context 'when expanded' do
+    context 'when the blob is expanded' do
       before do
-        viewer.expanded = true
+        blob.expand!
       end
 
       context 'when the blob size is larger than the size limit' do