Python BaseHTTPRequestHandler: Respond with JSON - python

I have a Python class that inherits BaseHTTPRequestHandler and implements the method do_POST.
I currently only succeed to respond with an integer status, e.g. 200, using the following command at the end of the method:
self.send_response(200)
I am trying to also send some string as a part of the response. How should I do it?

At least in my environment (Python 3.7) i have to use
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json_str.encode(encoding='utf_8'))
otherwise this error will be thrown:
TypeError: a bytes-like object is required, not 'str'

It turns out to be pretty simple, though there aren't many examples for it.
Just use:
self.wfile.write(YOUR_STRING_HERE)
Specifically for the case of json:
import json
json_string = json.dumps(YOUR_DATA_STRUCTURE_TO_CONVERT_TO_JSON)
self.wfile.write(json_string)

It's an old question. Still, if someone else might be wondering the same, here's my 2 cent.
If you are doing anything useful, apart from playing around with python, you should start looking for standard python frameworks to handle HTTP server operations, like Django or Flask.
That being said, there's a small stub that I use to act as a test server for my outgoing requests, which should answer your question. You can set any status code, header or response body by modifying it:
#!/usr/bin/env python
# Reflects the requests with dummy responses from HTTP methods GET, POST, PUT, and DELETE
# Written by Tushar Dwivedi (2017)
import json
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
request_path = self.path
print("\n----- Request Start ----->\n")
print("request_path :", request_path)
print("self.headers :", self.headers)
print("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
self.wfile.write(json.dumps({'hello': 'world', 'received': 'ok'}))
def do_POST(self):
request_path = self.path
# print("\n----- Request Start ----->\n")
print("request_path : %s", request_path)
request_headers = self.headers
content_length = request_headers.getheaders('content-length')
length = int(content_length[0]) if content_length else 0
# print("length :", length)
print("request_headers : %s" % request_headers)
print("content : %s" % self.rfile.read(length))
# print("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
self.wfile.write(json.dumps({'hello': 'world', 'received': 'ok'}))
do_PUT = do_POST
do_DELETE = do_GET
def main():
port = 8082
print('Listening on localhost:%s' % port)
server = HTTPServer(('', port), RequestHandler)
server.serve_forever()
if __name__ == "__main__":
parser = OptionParser()
parser.usage = ("Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
"Run:\n\n")
(options, args) = parser.parse_args()
main()
Again, even if you are just learning, and you even need to add 5-6 of if elses to the above to do what you are doing, it's better to do things right from the beginning, to avoid a lot of rework in future. Use a framework capable of handling boilerplate stuff for you.

Related

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
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.

Is there some caching or forking happening in `HTTPServer` or` BaseHTTPRequestHandler`?

It could be my code is wrongly implemented, but I'm finding that while I can serve up GET requests from literal data, I cannot update that data and have it shown as updated in subsequent GET requests. I also cannot have POST requests update the data.
So it behaves as though somewhere in Python's HTTPServer or BaseHTTPRequestHandler there's caching or forking happening.
Thanks in advance for looking it over, but, gently, no, I do not want to use a non-core 3.8 module or re-write with a wholly different framework or some Flask. I think this should work, but it's misbehaving in a way I can't spot why. If I were using C or Go's built in libraries it'd expect it would not be as much of a head scratcher (for me).
To demonstrate, you'd run the following python implementation, and load http://127.0.0.1:8081/ two or three times:
"""
A Quick test server on 8081.
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
import cgi
import json
import os
import sys
ADDR = '127.0.0.1'
PORT = 8081
def run(server_class=HTTPServer, handler_class=BaseHTTPRequestHandler):
server_address = (ADDR, PORT)
with server_class(server_address, handler_class) as httpd:
print("serving at", ADDR, "on", PORT, f"[ http://{ADDR}:{PORT} ]")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print(" stopping web server due to interrupt signal...")
httpd.socket.close()
class SimpleHandler(BaseHTTPRequestHandler):
"""
Implements responses to GET POST
"""
def __init__(self, request, client_address, server):
"""Sets up the server's memory, a favicon, and one text pseudo-file."""
self.files = {
'/oh': ['text/plain', "It's me", ],
'/favicon.ico': [
'image/svg+xml',
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><text y="1em" font-size="48">⁇</text></svg>',
],
}
self.head = '<link rel="icon" type="image/svg+xml" sizes="48x48" '\
'href="/favicon.ico">'
super(SimpleHandler, self).__init__(request, client_address, server)
def _set_headers(self, content_type='application/json', response=200):
self.send_response(response)
self.send_header("Content-type", content_type)
self.end_headers()
def _html(self, message, title='Simple Server', extra=""):
"""This generates HTML with `message` in the h1 of body."""
content = f"<html><head><title>{title}</title>{self.head}</head>" \
f"<body><h1>{message}</h1>{extra}</body></html>"
return content.encode("utf8") # NOTE: must return a bytes object!
def do_GET(self):
"""Respond to a GET request."""
if self.path == "/":
self._set_headers('text/html')
fnames = [f'<li>{fn}</li>' for fn in self.files.keys()]
fnames.sort()
self.wfile.write(self._html(
"Welcome",
extra='Try:'
'<ul>'
'<li>/hello</li>'
f'{"".join(fnames)}'
'</ul>'
))
elif self.path == "/hello":
self._set_headers('text/html')
self.wfile.write(self._html("hello you"))
elif self.path in self.files:
content_type, content = self.files[self.path]
self.send_response(200)
self._set_headers(content_type)
self.wfile.write(content.encode())
else:
self.send_error(404)
# Note this update doesn't seem to happen to the in memory dict.
self.files[f"/{len(self.files)}"] = [
"text/html", self._html(len(self.files))]
def do_HEAD(self):
if self.path in ["/", "/hello"]:
self._set_headers('text/html')
elif self.path in self.files:
content_type, _ = self.files[self.path]
self._set_headers(content_type)
else:
self.send_error(404)
def do_POST(self):
"""Should update pseudo-files with posted file contents."""
ctype, pdict = cgi.parse_header(
self.headers.get('content-type', self.headers.get_content_type()))
print("POSTED with content type", ctype)
content = None
if ctype == 'application/x-www-form-urlencoded':
print(" * This multipart/form-data method might not work")
content = {"content": str(self.rfile.read(int(self.headers['Content-Length'])).decode())}
elif ctype == 'multipart/form-data':
print(" * This multipart/form-data method might not work")
fields = cgi.parse_multipart(self.rfile, pdict)
content = {"content": fields.get('content')}
elif ctype == 'application/json':
data_string = self.rfile.read(int(self.headers['Content-Length']))
content = json.loads(data_string)
else:
self.send_error(404)
print(" * Received content:", content)
# Note this update doesn't seem to happen to the in memory dict.
self.files[self.path] = ['application/json', content]
self._set_headers(response=201)
self.wfile.write(json.dumps(content).encode())
if __name__ == '__main__':
print('FYI:')
print(' LANG =', os.getenv('LANG'))
print(' Default Charset Encoding =', sys.getdefaultencoding())
path_to_script = os.path.dirname(os.path.realpath(__file__))
print('Serving from path:', path_to_script)
os.chdir(path_to_script)
run(handler_class=SimpleHandler)
Even before loading http://127.0.0.1:8081/ one could try posting to add something to the self.files dict. E.G.
curl -v -H 'content-type: application/json' \
--data-binary '{"this": "should work"}' http://127.0.0.1:8081/new_file
And you can see the server respond, and also print the data recieved, which should now be in self.files and therefore the / should show it.
You can mix it up with:
curl -v --data-urlencode 'content={"this": "should work"}' http://127.0.0.1:8081/new_file2
But neither of these add a self.files['/new_file'] or '/new_file2', and it's just not clear why.
One should be able to request /new_file or /new_file2 and those instead are 404.
With the last lines in do_GET, multiple GET / requests should show more listed items.
$ curl http://127.0.0.1:8081
<html><head><title>Simple Server</title><link rel="icon" type="image/svg+xml" sizes="48x48" href="/favicon.ico"></head><body><h1>Welcome</h1>Try:<ul><li>/hello</li><li>/favicon.ico</li><li>/oh</li></ul></body></html>
$ curl http://127.0.0.1:8081
<html><head><title>Simple Server</title><link rel="icon" type="image/svg+xml" sizes="48x48" href="/favicon.ico"></head><body><h1>Welcome</h1>Try:<ul><li>/hello</li><li>/favicon.ico</li><li>/oh</li></ul></body></html>
While moving those lines that add a new key and value into self.files to the top of do_GET shows that it does update, but only one time, which just seems odder still:
$ curl http://127.0.0.1:8081
<html><head><title>Simple Server</title><link rel="icon" type="image/svg+xml" sizes="48x48" href="/favicon.ico"></head><body><h1>Welcome</h1>Try:<ul><li>/hello</li><li>/2</li><li>/favicon.ico</li><li>/oh</li></ul></body></html>
$ curl http://127.0.0.1:8081
<html><head><title>Simple Server</title><link rel="icon" type="image/svg+xml" sizes="48x48" href="/favicon.ico"></head><body><h1>Welcome</h1>Try:<ul><li>/hello</li><li>/2</li><li>/favicon.ico</li><li>/oh</li></ul></body></html>
Okay, it turns out that a new SimpleHandler is made for each request, therefore I had to move the self.files out to the outer scope and also be careful what is set up during SimpleHandler's __init__. And that basically makes the behavior as I had expected.

Get file from POST request using Python's BaseHTTPServer

I'm running the code below to get POST messages. How do I get the file (sent with POST) and how do I respond with HTTP status code 200 OK?
#!/usr/bin/env python
import ssl
import BaseHTTPServer, SimpleHTTPServer
from BaseHTTPServer import BaseHTTPRequestHandler
class HttpHandler(BaseHTTPRequestHandler):
def do_POST(self):
print "got POST message"
# how do I get the file here?
print self.request.FILES
httpd = BaseHTTPServer.HTTPServer(('localhost', 4443), HttpHandler)
httpd.socket = ssl.wrap_socket(httpd.socket, certfile='./server.pem', server_side=True)
httpd.serve_forever()
$curl -X POST -d #../some.file https://localhost:4443/resource --insecure
AttributeError: 'SSLSocket' object has no attribute 'FILES'
BaseHTTPRequestHandler will process the first line and the headers of the http request then leave the rest up to you.
You need to read the remainder of the request using BaseHTTPRequestHandler.rfile
You can use self.send_response(200) to respond with 200 OK
Given the curl command you have specified the following should answer your question:
class HttpHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
file_content = self.rfile.read(content_length)
# Do what you wish with file_content
print file_content
# Respond with 200 OK
self.send_response(200)
Note that by using -d #../some.file in your curl command you are saying "this is an ascii file, oh and strip out the newlines and carriage returns please", thus there could be differences between the file you are using and the data you get in the request. Your curl command does not emulate an html form file-upload like post request.
It is generally available in
request.FILES
dictionary

Python - BaseHTTPServer keep alive does not work

I have the following code for a simple BaseHTTPServer based server.
class myHandler(BaseHTTPRequestHandler):
#Handler for the GET requests
def do_GET(self):
# Parse the query_str
query_str = self.path.strip().lower()
if query_str.startswith("/download?"):
query_str = query_str[10:]
opts = urlparse.parse_qs(query_str)
# Send the html message and download file
self.protocol_version = 'HTTP/1.1'
self.send_response(200)
self.send_header("Content-type", 'text/html')
self.send_header("Content-length", 1)
self.end_headers()
self.wfile.write("0")
# Some code to do some processing
# ...
# -----------
self.wfile.write("1")
I was expecting the HTML page to show "1", but it shows "0". How can I update the response through keep alive?
I believe you are setting self.protocol_version to 'HTTP/1.1' too late. You are doing it in your do_GET() method, at which point your request handler has already been instantiated, and the server has already inspected that instance's protocol_version property.
Better to set it on the class:
class myHandler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
Not sure what you are trying to accomplish, but if you want the 1 to be sent, you need to set your content-length to 2 or remove it entirely. The 1 is not going to overwrite the 0, so you will see 01.
https://docs.python.org/2/library/basehttpserver.html
protocol_version
This specifies the HTTP protocol version used in responses. If set to 'HTTP/1.1', the server will permit HTTP persistent connections; however, your server must then include an accurate Content-Length header (using send_header()) in all of its responses to clients. For backwards compatibility, the setting defaults to 'HTTP/1.0'.
I faced same question. I tried set protocol_version in my do_METHOD() function which doesn't work.
My code look like this.
def _handle(self, method):
self.protocol_version = "HTTP/1.1"
# some code here
def do_GET(self):
self._handle("GET")
I used ss and tcpdump to detect network and finally find server will reset connection after send response although it use http/1.1.
So I try set protocol_version just under my class which inherited from standard library class and it works. Because of cost of time, I don't dive into source code. Hope it works for others.

Mootools Request getting "501 Unsupported method ('OPTIONS')" response

I have this mootools request:
new Request({
url: 'http://localhost:8080/list',
method: 'get',
}).send();
and a small python server that handles it with this:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import subprocess
class HttpHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/list':
self.list()
else:
self._404()
def list(self):
self.response200()
res = "some string"
self.wfile.write(res)
def _404(self):
self.response404()
self.wfile.write("404\n")
def response200(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Headers', 'X-Request, X-Requested-With')
self.send_header('Content-type', 'application/json')
self.end_headers()
def response404(self):
self.send_response(404)
self.send_header('Content-type', 'application/json')
self.end_headers()
def main():
try:
server = HTTPServer(('', 8080), HttpHandler)
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
if __name__ == '__main__':
main()
When I attempt to make this request, I get these errors:
OPTIONS http://localhost:8080/ 501 (Unsupported method ('OPTIONS'))
XMLHttpRequest cannot load http://localhost:8080/. Origin null is not allowed by Access-Control-Allow-Origin.
I'm not sure what's going on. Can someone help me out??
exactly as the response string tells you: OPTIONS http://localhost:8080/ 501 (Unsupported method ('OPTIONS'))
When javascript attempts to request a resource from another origin, modern browsers first ask the other server, the target, if it is ok to make that request from another origin, that's exactly what the Access-Control* headers do. but this request does not happen in a normal GET, since that would be actually performing the request anyway, and instead use the OPTIONS method, which exists for the sole reason to inform clients what they are allowed to do, without actually doing it.
So, you need a do_OPTIONS method, which might look something like:
def do_OPTIONS(self):
if self.path in ('*', '/list'):
self.send_response(200)
self.send_header('Allow', 'GET, OPTIONS')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Headers', 'X-Request, X-Requested-With')
else:
self.send_response(404)
self.send_header('Content-Length', '0')
self.end_headers()

Categories