pythonjsonhttpgetsimplehttpserver

Create HTTP get response with attached binary data file and json formatted metadata


I'm using BaseHTTPServer.BaseHTTPRequestHandler in order to implement my server.

currently I repsonse to get request with merely binary data file.

self.send_response(200)
self.send_header("Content-Type", 'application/octet-stream')
self.send_header("Content-Disposition", 'attachment; filename="{}"'.format(os.path.basename(FILEPATH)))
fs = os.fstat(f.fileno())
self.send_header("Content-Length", str(fs.st_size))
self.end_headers()

Now it's requested to add another section which include some short json formatted configuration data (i.e. {'status': 'ok', 'type': 'keepalive'}) and i'd rather pass this information on the same response separated by unique http header or by the http body.

What is the best way to do so ? I'd like to know how to extend my code to support this.

Thanks


Solution

  • There's lots of ways to do this, I think the best choice is going to depend on what your receiving side is capable of understanding most easily.

    The most literal interpretation would be to use content-type multipart/mixed https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html but you're probably going to have to write your own parsing on the receiving end. I don't know if this is exactly to spec, but it gets the idea across:

    from http.server import BaseHTTPRequestHandler
    import http.server
    import socketserver
    import string
    import random
    import io
    
    PORT = 8000
    
    class ResponsePart:
        def __init__(self, content, content_type):
            self.content = content.encode('utf-8')
            self.content_type = content_type.encode('utf-8')
    
    class Mine(http.server.BaseHTTPRequestHandler):
        def get_separator(self, parts):
            while True:
                boundary = []
                for i in range(32):
                    boundary.append(random.choice(string.digits + string.ascii_letters))
                boundary = ''.join(boundary).encode('ascii')
                for part in parts:
                    if boundary in part:
                        break
                else:
                    return boundary
    
        def do_GET(self):
            responses = [
                    ResponsePart('abc123', 'Content-type: application/octet-stream'),
                    ResponsePart('{"a":"b"}', 'Content-type: application/json'),
            ]
            boundary = self.get_separator([r.content for r in responses])
            self.send_response(200)
            self.send_header("Content-Type", 'multipart/mixed; boundary=' + boundary.decode('ascii'))
            self.end_headers()
    
            for piece in responses:
                self.wfile.write(b'--')
                self.wfile.write(boundary)
                self.wfile.write(b'\r\n')
                self.wfile.write(piece.content_type)
                self.wfile.write(b'\r\n')
                self.wfile.write(piece.content)
                self.wfile.write(b'\r\n')
    
    Handler = Mine
    
    with socketserver.TCPServer(("", PORT), Handler) as httpd:
        httpd.serve_forever()
    

    With that out of the way, I'd do this probably using JSON or something so that you're returning a single consistent content-type:

    from http.server import BaseHTTPRequestHandler
    import http.server
    import socketserver
    import string
    import random
    import io
    import json
    
    PORT = 8000
    
    class Mine(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            response = {
                    'filedata': 'abc123',
                    'status': {"a":"b"},
            }
            output_data = json.dumps(response).encode('utf-8')
    
            self.send_response(200)
            self.send_header("Content-Type", 'application/octet-stream')
            self.send_header("Content-Length", str(len(output_data)))
            self.end_headers()
    
            self.wfile.write(output_data)
    
    Handler = Mine
    
    with socketserver.TCPServer(("", PORT), Handler) as httpd:
        httpd.serve_forever()
    

    This is going to be far easier to handle on the receiving end, one json decode and you're done.