QHttpMultiPart generates SIGSEGV when used multiple times
-
Hello Everyone!
Using the following code, I am trying to upload images to a FastAPI http server:from pathlib import Path import sys from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLineEdit, QPushButton, QTextEdit, QFileDialog, \ QApplication from PyQt5.QtCore import pyqtSignal, QObject, QUrl from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart class Client(QObject): replyReceived = pyqtSignal(str) def __init__(self): super().__init__() self.nam = QNetworkAccessManager() self.nam.finished.connect(self.on_reply) def make_request(self, url, data, filename): print(f"Uploading file to {url}") self.request = QNetworkRequest(url) self.multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) for key, value in (data or {}).items(): http_part = QHttpPart() http_part.setHeader( QNetworkRequest.ContentDispositionHeader, f'form-data; name="{key}"' ) http_part.setBody(value.encode('utf-8')) self.multipart.append(http_part) if filename: file_part = QHttpPart() filedata = open(filename, 'rb').read() file_part.setHeader( QNetworkRequest.ContentDispositionHeader, f'form-data; name="uploaded_file"; filename="{Path(filename).name}"' ) file_part.setBody(filedata) self.multipart.append(file_part) self.nam.post(self.request, self.multipart) def on_reply(self, reply): reply_bytes = reply.readAll() reply_string = bytes(reply_bytes).decode('utf-8') self.replyReceived.emit(reply_string) class MainWindow(QMainWindow): def __init__(self): super().__init__() widget = QWidget(minimumWidth=600) self.setCentralWidget(widget) widget.setLayout(QVBoxLayout()) self.url = QLineEdit('http://127.0.0.1:8000/photos') self.fname = QPushButton('(Choose File...)', clicked=self.on_file_btn) submit = QPushButton('Send File', clicked=self.submit) response = QTextEdit(readOnly=True) for w in (self.url, self.fname, submit, response): widget.layout().addWidget(w) self.poster = Client() self.poster.replyReceived.connect(response.setText) self.show() def on_file_btn(self): filename, accepted = QFileDialog.getOpenFileName() if accepted: self.fname.setText(filename) def submit(self): url = QUrl(self.url.text()) for i in range(0, 1): filename = '205520_20220911191143.png' if filename == '(No File)': filename = None data = {'var1': 'spam'} self.poster.make_request(url, data, filename) if __name__ == '__main__': app = QApplication(sys.argv) main_window = MainWindow() sys.exit(app.exec())If used once, this works as intended. However if I increase the steps in
line 71torange(0, 10)for examle, I get:
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV).
I believe that there is something blocking and objects are being deleted in the background, however I am unable to solve the problem here.
I would be more than grateful if someone could give more insight to my issue!
Thank you in advance!
Boris -
@borisruna said in QHttpMultiPart generates SIGSEGV when used multiple times:
QHttpMultiPart
With the great help of "The Compiler / Florian Bruhin" from #qt-pyside on libera.chat, using
gdb -ex r --args python3 app.py, we got the following result:#0 0x00007ffff59b5807 in QMetaObject::cast(QObject const*) const () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Core.so.5 No symbol table info available. #1 0x00007ffff58e4dec in QNonContiguousByteDeviceFactory::createShared(QIODevice*) () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Core.so.5 No symbol table info available. #2 0x00007ffff21eb1cd in QNetworkReplyHttpImplPrivate::createUploadByteDevice() () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Network.so.5 No symbol table info available. #3 0x00007ffff21f18ef in QNetworkReplyHttpImplPrivate::postRequest(QNetworkRequest const&) () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Network.so.5 No symbol table info available. #4 0x00007ffff21f25c2 in QNetworkReplyHttpImplPrivate::start(QNetworkRequest const&) () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Network.so.5 --Type <RET> for more, q to quit, c to continue without paging-- No symbol table info available. #5 0x00007ffff21f28db in QNetworkReplyHttpImplPrivate::_q_startOperation() () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Network.so.5 No symbol table info available. #6 0x00007ffff228ffb9 in QNetworkReplyHttpImpl::qt_static_metacall(QObject*, QMetaObject::Call, int, void**) () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Network.so.5 No symbol table info available. #7 0x00007ffff59dada3 in QObject::event(QEvent*) () from /home/b/test/venv/lib/python3.8/site-packages/PyQt5/Qt5/lib/libQt5Core.so.5 No symbol table info available.and concluded that objects are garbage collected by Python during the upload, while Qt still needs them.
One suggestion was to store the requests and multipart objects in a temporary list, however this would need then a proper cleanup to avoid a memory leak.
Still working on this, will reply when finished. However if someone has other suggestions I would be more than happy to discuss them.
Once again, many thanks to "The - Compiler" ! -
I also found this https://github.com/mbruel/qHttpPostFile/blob/master/HttpPoster.cpp#L54, which in my case translates to :
from pathlib import Path import sys from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLineEdit, QPushButton, QTextEdit, QFileDialog, \ QApplication from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QEventLoop, QFile from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart class Client(QObject): replyReceived = pyqtSignal(str) def __init__(self): super().__init__() self.nam = QNetworkAccessManager() self.nam.finished.connect(self.on_reply) def _create_multipart(self, url, data, filename): multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) for key, value in (data or {}).items(): http_part = QHttpPart() http_part.setHeader( QNetworkRequest.ContentDispositionHeader, f'form-data; name="{key}"' ) http_part.setBody(value.encode('utf-8')) multipart.append(http_part) if filename: file_part = QHttpPart() filedata = open(filename, 'rb').read() file_part.setHeader( QNetworkRequest.ContentDispositionHeader, f'form-data; name="uploaded_file"; filename="{Path(filename).name}"' ) file_part.setBody(filedata) multipart.append(file_part) return multipart # self.nam.post(request, multipart) # self.requests.append(request) # self.multiparts.append(multipart) def upload(self, url, data, filename): url = QUrl(url) multipart = self._create_multipart(url=url, data=data, filename=filename) request = QNetworkRequest(url) reply = self.nam.post(request, multipart) multipart.setParent(reply) print(f'Uploading file to {url}') def on_reply(self, reply): reply_bytes = reply.readAll() reply_string = bytes(reply_bytes).decode('utf-8') print(reply_string) self.replyReceived.emit(reply_string) class MainWindow(QMainWindow): def __init__(self): super().__init__() widget = QWidget(minimumWidth=600) self.setCentralWidget(widget) widget.setLayout(QVBoxLayout()) self.url = QLineEdit('http://127.0.0.1:8000/photos') self.fname = QPushButton('(Choose File...)', clicked=self.on_file_btn) submit = QPushButton('Send File', clicked=self.submit) response = QTextEdit(readOnly=True) for w in (self.url, self.fname, submit, response): widget.layout().addWidget(w) self.poster = Client() self.poster.replyReceived.connect(response.setText) self.show() def on_file_btn(self): filename, accepted = QFileDialog.getOpenFileName() if accepted: self.fname.setText(filename) def submit(self): url = QUrl(self.url.text()) for i in range(0, 10): filename = '205520_20220911191143.png' if filename == '(No File)': filename = None data = {'var1': 'spam'} self.poster.upload(url, data, filename) if __name__ == '__main__': app = QApplication(sys.argv) main_window = MainWindow() sys.exit(app.exec())This method seems to work well (at least for my tests so far)