1

I'm trying to port a working curl solution to plain python.

I'm uploading a file (pdf in this case) to a documented API endpoint: https://developers.lexoffice.io/docs/#vouchers-endpoint-upload-a-file-to-a-voucher

The example from the documentation in curl works flawlessly:

retStream = os.popen('curl https://api.lexoffice.io/v1/vouchers/' + cr.json()['bookkeepingVoucherId'] + '/files -X POST'
            + ' -H "Authorization: Bearer ' + Settings.LEXOFFICE_CONFIG['api-key'] + '" -H "Content-Type: multipart/form-data"'
            + ' -H "Accept: application/json" -F "file=@' + zfo.filename + '"')

Now the same in Python, it fails and gets an unspecific 500:

   def _it__(self, apiKey):
        self.__apiKey = apiKey
        self.__headers = {
            'Authorization': 'Bearer ' + self.__apiKey,
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }
[...]
    def uploadFileToAVoucher(self, voucherId, filename):
        heads = self.__headers.copy()
        heads['Content-Type'] = 'multipart/form-data'
        print(filename)
        files = {
            'file': (filename, open(filename, 'rb'), 'application/pdf')
        }
        return requests.post(LexofficeApi.API_URL + '/v1/vouchers/' + voucherId + '/files',
                            files=files, headers=heads)

The API-Key is identical (and other API calls work perfectly, the basic structure must be correct). I even dumped the curl outgoing http body with --trace-ascii and compared it to the python request. The exact same sequence.

I have no clue what Python does differently. Any ideas?

curl trace:

=> Send header, 315 bytes (0x13b)
0000: POST /v1/vouchers/<removed>/files HTT
0040: P/2
0045: Host: api.lexoffice.io
005d: user-agent: curl/7.68.0
0076: authorization: Bearer <removed>
00b2: accept: application/json
00cc: content-length: 35015
00e3: content-type: multipart/form-data; boundary=--------------------
0123: ----a52203785a14b388
0139: 
<= Recv SSL data, 5 bytes (0x5)
0000: ....@
== Info: Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
=> Send SSL data, 5 bytes (0x5)
0000: ....!
=> Send SSL data, 5 bytes (0x5)
0000: ...@.
=> Send SSL data, 5 bytes (0x5)
0000: ....!
=> Send SSL data, 5 bytes (0x5)
0000: ...@.
=> Send SSL data, 5 bytes (0x5)
0000: ....!
=> Send SSL data, 5 bytes (0x5)
0000: .....
=> Send data, 35015 bytes (0x88c7)
0000: --------------------------a52203785a14b388
002c: Content-Disposition: form-data; name="file"; filename="<rem
006c: moved>"
0090: Content-Type: application/pdf
00af: 
00b1: %PDF-1.4.%.....1 0 obj.<</Creator (Chromium)./Producer (Skia/PDF
00f1:  m93)./CreationDate (D:20211004001328+00'00')./ModDate (D:202110
0131: 04001328+00'00')>>.endobj.3 0 obj.<</ca 1./BM /Normal>>.endobj.5
0171:  0 obj.<</ca .9333./BM /Normal>>.endobj.8 0 obj.<</Filter /Flate
01b1: Decode./Length 6736>> stream.x..]]...q.._q..T.7E.0.k{}.....:H..E
01f1: .4n.....t..3..sL...^{..45_.g8.R..tX.y....'..l..d..........+3...
[...]

python request body:

b'--ae550894d8422832a377fdd24f109898\r\nContent-Disposition: form-data; name="file"; filename="<removed>"\r\nContent-Type: application/pdf\r\n\r\n%PDF-1.4\n%\xd3\xeb\xe9\xe1\n1 0 obj\n<</Creator (Chromium)\n/Producer (Skia/PDF m93)\n/CreationDate (D:20211004002626+00\'00\')\n/ModDate (D:20211004002626+00\'00\')>>\nendobj\n3 0 obj\n<</ca 1\n/BM /Normal>>\nendobj\n5 0 obj\n<</ca .9333\n/BM /Normal>>\nendobj\n8 0 obj\n<</Filter /FlateDecode\n/Length 6736>> stream\nx\x9c\xed]]\xc

Looks very much the same to me.

MPW
  • 161
  • 1
  • 3
  • 16
  • Can you print out the Python POST request, incl. headers, as described [here](https://stackoverflow.com/a/23816211/530160) and add it to your question? Would be nice to compare the curl and Python ones. – Nick ODell Oct 04 '21 at 01:55
  • When I have issues like this, I will try to find a minimal similar request that I can program "equivalent" curl-vs-python calls an make sure those short one works. For example, requests with no auth or no arguments, and if that works, add complexity to find the breakage. – RufusVS Oct 04 '21 at 02:06

1 Answers1

0

requests.post doesn't set the Content-Length, it's always 0. The LexOffice-API seems to be picky about this.

A workaround with pycurl works. Here's a generic version of the question: python requests.post header content-length always 0

MPW
  • 161
  • 1
  • 3
  • 16