diff --git a/src/core/url_request_custom_job.cpp b/src/core/url_request_custom_job.cpp
index 47c9b3b4c00ddbd983e768751a8a6497ff1b0513..b49135f79a6f62578ef1cff1dc047c7b67166fa7 100644
--- a/src/core/url_request_custom_job.cpp
+++ b/src/core/url_request_custom_job.cpp
@@ -56,6 +56,9 @@ URLRequestCustomJob::URLRequestCustomJob(URLRequest *request,
     , m_proxy(new URLRequestCustomJobProxy(this, scheme, adapter))
     , m_device(nullptr)
     , m_error(0)
+    , m_pendingReadSize(0)
+    , m_pendingReadPos(0)
+    , m_pendingReadBuffer(nullptr)
 {
 }
 
@@ -83,6 +86,12 @@ void URLRequestCustomJob::Kill()
     DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
     if (m_device && m_device->isOpen())
         m_device->close();
+    if (m_pendingReadBuffer) {
+        m_pendingReadBuffer->Release();
+        m_pendingReadBuffer = nullptr;
+        m_pendingReadSize = 0;
+        m_pendingReadPos = 0;
+    }
     m_device = nullptr;
     content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
                                      base::Bind(&URLRequestCustomJobProxy::release,
@@ -127,13 +136,64 @@ int URLRequestCustomJob::ReadRawData(IOBuffer *buf, int bufSize)
     if (m_error)
         return m_error;
     qint64 rv = m_device ? m_device->read(buf->data(), bufSize) : -1;
-    if (rv >= 0) {
+    if (rv > 0) {
         return static_cast<int>(rv);
+    } else if (rv == 0) {
+        // Returning zero is interpreted as EOF by Chromium, so only
+        // return zero if we are the end of the file.
+        if (m_device->atEnd())
+            return 0;
+        // Otherwise return IO_PENDING and call ReadRawDataComplete when we have data
+        // for them.
+        buf->AddRef();
+        m_pendingReadPos = 0;
+        m_pendingReadSize = bufSize;
+        m_pendingReadBuffer = buf;
+        return ERR_IO_PENDING;
     } else {
         // QIODevice::read might have called fail on us.
         if (m_error)
             return m_error;
+        if (m_device && m_device->atEnd())
+            return 0;
         return ERR_FAILED;
     }
 }
+
+void URLRequestCustomJob::notifyReadyRead()
+{
+    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
+    if (!m_device)
+        return;
+    if (!m_pendingReadSize)
+        return;
+    Q_ASSERT(m_pendingReadBuffer);
+    if (!m_pendingReadBuffer)
+        return;
+
+    qint64 rv = m_device->read(m_pendingReadBuffer->data() + m_pendingReadPos, m_pendingReadSize - m_pendingReadPos);
+    if (rv == 0)
+        return;
+    if (rv < 0) {
+        if (m_error)
+            rv = m_error;
+        else if (m_device->atEnd())
+            rv = 0;
+        else
+            rv = ERR_FAILED;
+    } else {
+        m_pendingReadPos += rv;
+        if (m_pendingReadPos < m_pendingReadSize && !m_device->atEnd())
+            return;
+        rv = m_pendingReadPos;
+    }
+    // killJob may be called from ReadRawDataComplete
+    net::IOBuffer *buf = m_pendingReadBuffer;
+    m_pendingReadBuffer = nullptr;
+    m_pendingReadSize = 0;
+    m_pendingReadPos = 0;
+    ReadRawDataComplete(rv);
+    buf->Release();
+}
+
 } // namespace
diff --git a/src/core/url_request_custom_job.h b/src/core/url_request_custom_job.h
index 68a834d48b4a5e3f466305cb5256181ed2719554..021cf320436beb67ddc844a5d65ce9a566fa3c8a 100644
--- a/src/core/url_request_custom_job.h
+++ b/src/core/url_request_custom_job.h
@@ -70,12 +70,16 @@ protected:
     virtual ~URLRequestCustomJob();
 
 private:
+    void notifyReadyRead();
     scoped_refptr<URLRequestCustomJobProxy> m_proxy;
     std::string m_mimeType;
     std::string m_charset;
     GURL m_redirect;
     QIODevice *m_device;
     int m_error;
+    int m_pendingReadSize;
+    int m_pendingReadPos;
+    net::IOBuffer *m_pendingReadBuffer;
 
     friend class URLRequestCustomJobProxy;
 
diff --git a/src/core/url_request_custom_job_delegate.cpp b/src/core/url_request_custom_job_delegate.cpp
index 14de9a812d02e6bd1a3bd54ea1b1cd91755bf196..6b82cebb57df66c2693abe9479cb741b3bf09263 100644
--- a/src/core/url_request_custom_job_delegate.cpp
+++ b/src/core/url_request_custom_job_delegate.cpp
@@ -73,16 +73,23 @@ QByteArray URLRequestCustomJobDelegate::method() const
 
 void URLRequestCustomJobDelegate::reply(const QByteArray &contentType, QIODevice *device)
 {
+    if (device)
+        QObject::connect(device, &QIODevice::readyRead, this, &URLRequestCustomJobDelegate::slotReadyRead);
     content::BrowserThread::PostTask(content::BrowserThread::IO, FROM_HERE,
                                      base::Bind(&URLRequestCustomJobProxy::reply,
                                                 m_proxy,contentType.toStdString(),device));
 }
 
+void URLRequestCustomJobDelegate::slotReadyRead()
+{
+    content::BrowserThread::PostTask(content::BrowserThread::IO, FROM_HERE,
+                                     base::Bind(&URLRequestCustomJobProxy::readyRead, m_proxy));
+}
+
 void URLRequestCustomJobDelegate::abort()
 {
     content::BrowserThread::PostTask(content::BrowserThread::IO, FROM_HERE,
-                                     base::Bind(&URLRequestCustomJobProxy::abort,
-                                                m_proxy));
+                                     base::Bind(&URLRequestCustomJobProxy::abort, m_proxy));
 }
 
 void URLRequestCustomJobDelegate::redirect(const QUrl &url)
diff --git a/src/core/url_request_custom_job_delegate.h b/src/core/url_request_custom_job_delegate.h
index eb99f3576d5af0b8d6eb4dda3ec13e2189c2f39a..3f5e6d5913ff7ed0655215f81b0c6b4b13c3be88 100644
--- a/src/core/url_request_custom_job_delegate.h
+++ b/src/core/url_request_custom_job_delegate.h
@@ -74,6 +74,9 @@ public:
     void abort();
     void fail(Error);
 
+private Q_SLOTS:
+    void slotReadyRead();
+
 private:
     URLRequestCustomJobDelegate(URLRequestCustomJobProxy *proxy,
                                 const QUrl &url,
diff --git a/src/core/url_request_custom_job_proxy.cpp b/src/core/url_request_custom_job_proxy.cpp
index d53602c857762b83b67cf0458a520456ce817db1..832d3d11e567cac1a174c5d81521ba652103030c 100644
--- a/src/core/url_request_custom_job_proxy.cpp
+++ b/src/core/url_request_custom_job_proxy.cpp
@@ -144,6 +144,13 @@ void URLRequestCustomJobProxy::fail(int error)
     // else we fail on the next read, or the read that might already be in progress
 }
 
+void URLRequestCustomJobProxy::readyRead()
+{
+    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
+    if (m_job)
+        m_job->notifyReadyRead();
+}
+
 void URLRequestCustomJobProxy::initialize(GURL url, std::string method)
 {
     DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
diff --git a/src/core/url_request_custom_job_proxy.h b/src/core/url_request_custom_job_proxy.h
index df7171f5e7acfb3615a9ffb73ca9e105c2bb9ccf..3ea30a4e12e7131b698b2c6ef3cc886df8bed4d6 100644
--- a/src/core/url_request_custom_job_proxy.h
+++ b/src/core/url_request_custom_job_proxy.h
@@ -63,6 +63,7 @@ public:
                              QWeakPointer<const BrowserContextAdapter> adapter);
     ~URLRequestCustomJobProxy();
 
+    // Called from URLRequestCustomJobDelegate via post:
     //void setReplyCharset(const std::string &);
     void reply(std::string mimeType, QIODevice *device);
     void redirect(GURL url);
@@ -70,12 +71,13 @@ public:
     void fail(int error);
     void release();
     void initialize(GURL url, std::string method);
+    void readyRead();
 
-    //IO thread owned
+    // IO thread owned:
     URLRequestCustomJob *m_job;
     bool m_started;
 
-    //UI thread owned
+    // UI thread owned:
     std::string m_scheme;
     URLRequestCustomJobDelegate *m_delegate;
     QWeakPointer<const BrowserContextAdapter> m_adapter;
diff --git a/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp b/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp
index 093bc2e4362bbc03a4d02375262755ae87649bb3..4001051525d45398c126b8a2c8a203c23c316d81 100644
--- a/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp
+++ b/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp
@@ -49,6 +49,7 @@ private Q_SLOTS:
     void urlSchemeHandlers();
     void urlSchemeHandlerFailRequest();
     void urlSchemeHandlerFailOnRead();
+    void urlSchemeHandlerStreaming();
     void customUserAgent();
     void httpAcceptLanguage();
     void downloadItem();
@@ -178,6 +179,74 @@ public:
     }
 };
 
+class StreamingIODevice : public QIODevice {
+    Q_OBJECT
+public:
+    StreamingIODevice(QObject *parent) : QIODevice(parent), m_bytesRead(0), m_bytesAvailable(0)
+    {
+        setOpenMode(QIODevice::ReadOnly);
+        m_timer.start(100, this);
+    }
+    bool isSequential() const override { return true; }
+    qint64 bytesAvailable() const override
+    { return m_bytesAvailable; }
+    bool atEnd() const override
+    {
+        return (m_data.size() >= 1000 && m_bytesRead >= 1000);
+    }
+protected:
+    void timerEvent(QTimerEvent *) override
+    {
+        QMutexLocker lock(&m_mutex);
+        m_bytesAvailable += 200;
+        m_data.append(200, 'c');
+        emit readyRead();
+        if (m_data.size() >= 1000) {
+            m_timer.stop();
+            emit readChannelFinished();
+        }
+    }
+
+    qint64 readData(char *data, qint64 maxlen) override
+    {
+        QMutexLocker lock(&m_mutex);
+        qint64 len = qMin(qint64(m_bytesAvailable), maxlen);
+        if (len) {
+            memcpy(data, m_data.constData() + m_bytesRead, len);
+            m_bytesAvailable -= len;
+            m_bytesRead += len;
+        } else if (m_data.size() > 0)
+            return -1;
+
+        return len;
+    }
+    qint64 writeData(const char *, qint64) override
+    {
+        return 0;
+    }
+
+private:
+    QMutex m_mutex;
+    QByteArray m_data;
+    QBasicTimer m_timer;
+    int m_bytesRead;
+    int m_bytesAvailable;
+};
+
+class StreamingUrlSchemeHandler : public QWebEngineUrlSchemeHandler
+{
+public:
+    StreamingUrlSchemeHandler(QObject *parent = nullptr)
+        : QWebEngineUrlSchemeHandler(parent)
+    {
+    }
+
+    void requestStarted(QWebEngineUrlRequestJob *job)
+    {
+        job->reply("text/plain;charset=utf-8", new StreamingIODevice(job));
+    }
+};
+
 static bool loadSync(QWebEngineView *view, const QUrl &url, int timeout = 5000)
 {
     // Ripped off QTRY_VERIFY.
@@ -310,6 +379,22 @@ void tst_QWebEngineProfile::urlSchemeHandlerFailOnRead()
     QCOMPARE(toPlainTextSync(view.page()), QString());
 }
 
+void tst_QWebEngineProfile::urlSchemeHandlerStreaming()
+{
+    StreamingUrlSchemeHandler handler;
+    QWebEngineProfile profile;
+    profile.installUrlSchemeHandler("stream", &handler);
+    QWebEngineView view;
+    QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
+    view.setPage(new QWebEnginePage(&profile, &view));
+    view.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
+    view.load(QUrl(QStringLiteral("stream://whatever")));
+    QVERIFY(loadFinishedSpy.wait());
+    QByteArray result;
+    result.append(1000, 'c');
+    QCOMPARE(toPlainTextSync(view.page()), QString::fromLatin1(result));
+}
+
 void tst_QWebEngineProfile::customUserAgent()
 {
     QString defaultUserAgent = QWebEngineProfile::defaultProfile()->httpUserAgent();