pythonjsonpostpython-3.6httplib

Trying to send Python HTTPConnection content after accepting 100-continue header


I've been trying to debug a Python script I've inherited. It's trying to POST a CSV to a website via HTTPLib. The problem, as far as I can tell, is that HTTPLib doesn't handle receiving a 100-continue response, as per python http client stuck on 100 continue. Similarly to that post, this "Just Works" via Curl, but for various reasons we need this to run from a Python script.

I've tried to employ the work-around as detailed in an answer on that post, but I can't find a way to use that to submit the CSV after accepting the 100-continue response.

The general flow needs to be like this:

Here's the code in its current state, with my 10+ other commented remnants of other attempted workarounds removed:

#!/usr/bin/env python

import os
import ssl
import http.client
import binascii
import logging
import json

#classes taken from https://stackoverflow.com/questions/38084993/python-http-client-stuck-on-100-continue
class ContinueHTTPResponse(http.client.HTTPResponse):
    def _read_status(self, *args, **kwargs):
        version, status, reason = super()._read_status(*args, **kwargs)
        if status == 100:
            status = 199
        return version, status, reason

    def begin(self, *args, **kwargs):
        super().begin(*args, **kwargs)
        if self.status == 199:
            self.status = 100

    def _check_close(self, *args, **kwargs):
        return super()._check_close(*args, **kwargs) and self.status != 100


class ContinueHTTPSConnection(http.client.HTTPSConnection):
    response_class = ContinueHTTPResponse

    def getresponse(self, *args, **kwargs):
        logging.debug('running getresponse')
        response = super().getresponse(*args, **kwargs)
        if response.status == 100:
            setattr(self, '_HTTPConnection__state', http.client._CS_REQ_SENT)
            setattr(self, '_HTTPConnection__response', None)
        return response


def uploadTradeIngest(ingestFile, certFile, certPass, host, port, url):
  boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
  headers = {
    "accept": "application/json",
    "Content-Type": "multipart/form-data; boundary=%s" % boundary,
    "Expect": "100-continue",
  }

  context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
  context.load_cert_chain(certfile=certFile, password=certPass)
  connection = ContinueHTTPSConnection(
    host, port=port, context=context)

  with open(ingestFile, "r") as fh:
    ingest = fh.read()
  ## Create form-data boundary
  ingest = "--%s\r\nContent-Disposition: form-data; " % boundary + \
    "name=\"file\"; filename=\"%s\"" % os.path.basename(ingestFile) + \
    "\r\n\r\n%s\r\n--%s--\r\n" % (ingest, boundary)

  print("pre-request")
  connection.request(
    method="POST", url=url, headers=headers)
  print("post-request")
  #resp = connection.getresponse()

  resp = connection.getresponse()

  if resp.status == http.client.CONTINUE:
    resp.read()

    print("pre-send ingest")
    ingest = json.dumps(ingest)
    ingest = ingest.encode()
    print(ingest)
    connection.send(ingest)
    print("post-send ingest")
    resp = connection.getresponse()
    print("response1")
    print(resp)
    print("response2")
    print(resp.read())
    print("response3")
    return resp.read()

But this simply returns a 400 "Bad Request" response. The problem (I think) lies with the formatting and type of the "ingest" variable. If I don't run it through json.dumps() and encode() then the HTTPConnection.send() method rejects it:

ERROR: Got error: memoryview: a bytes-like object is required, not 'str'

I had a look at using the Requests library instead, but I couldn't get it to use my local certificate bundle to accept the site's certificate. I have a full chain with an encrypted key, which I did decrypt, but still ran into constant SSL_VERIFY errors from Requests. If you have a suggestion to solve my current problem with Requests, I'm happy to go down that path too.

How can I use HTTPLib or Requests (or any other libraries) to achieve what I need to achieve?


Solution

  • In case anyone comes across this problem in future, I ended up working around it with a bit of a kludge. I tried HTTPLib, Requests, and URLLib3 are all known to not handle the 100-continue header, so... I just wrote a Python wrapper around Curl via the subprocess.run() function, like this:

    def sendReq(upFile):
        sendFile=f"file=@{upFile}"
    
        completed = subprocess.run([
        curlPath,
        '--cert',
        args.cert,
        '--key',
        args.key,
        targetHost,
        '-H',
        'accept: application/json',
        '-H',
        'Content-Type: multipart/form-data',
        '-H',
        'Expect: 100-continue',
        '-F',
        sendFile,
         '-s'
        ], stdout=subprocess.PIPE, universal_newlines=True)
    
        return completed.stdout
    

    The only issue I had with this was that it fails if Curl was built against the NSS libraries, which I resolved by including a statically-built Curl binary with the package, the path to which is contained in the curlPath variable in the code. I obtained this binary from this Github repo.