I needed a throwaway HTTP server for about thirty seconds.
The job was a one-time OAuth handshake. Provider redirects the browser to a localhost URL with a ?code= on it, you swap that code for a token, done. The codes expire in roughly thirty seconds, and I'd already lost two of them to the time it takes a human to copy a string out of an address bar and paste it into a terminal. So: listen on the redirect port, grab the code the moment it lands, exchange it immediately, shut down.
Twenty lines with http.server. It printed "listening on http://localhost:8000/callback". I clicked approve. The browser landed on the callback and showed a 404.
A 404 is a strange thing to get from a server that only knows how to return 200 and 204.
The port was already taken
I run Laragon, which runs Apache, which owns :8000. Apache answered the browser. My handler never saw the request.
That part is obvious in hindsight. What wasn't obvious is that my server didn't fail to start. It bound without complaint, printed its startup line, and then waited for a connection that was being handed to another process.
I'd wrapped the bind in a try/except specifically to catch OSError: address already in use, because that was the failure I could see coming. The except never ran.
Why the bind succeeds
socketserver.TCPServer sets allow_reuse_address = False. http.server.HTTPServer overrides it:
>>> import socketserver, http.server
>>> socketserver.TCPServer.allow_reuse_address
False
>>> http.server.HTTPServer.allow_reuse_address
True
That flag becomes SO_REUSEADDR on the socket, and SO_REUSEADDR does not mean the same thing on Windows as it does on Linux.
On Linux it mostly lets you rebind a port still sitting in TIME_WAIT from a previous process, which is genuinely useful and is why HTTPServer turns it on: restart your dev server without waiting a minute.
On Windows it's broader. It lets you bind an address another socket is actively listening on. Both binds succeed, and which socket receives an incoming connection isn't something you get to decide.
Two servers on the same address, no error:
import http.server, threading, urllib.request
PORT = 8931
class A(http.server.BaseHTTPRequestHandler):
ident = b"A"
def do_GET(self):
self.send_response(200)
self.send_header("Content-Length", str(len(self.ident)))
self.end_headers()
self.wfile.write(self.ident)
def log_message(self, *a): pass
class B(A):
ident = b"B"
first = http.server.HTTPServer(("127.0.0.1", PORT), A)
threading.Thread(target=first.serve_forever, daemon=True).start()
second = http.server.HTTPServer(("127.0.0.1", PORT), B) # no exception
threading.Thread(target=second.serve_forever, daemon=True).start()
print(urllib.request.urlopen(f"http://127.0.0.1:{PORT}/").read())
On Windows that prints b'A'. Both servers are live, and the requests all go to whichever one bound first. The second process is running fine and simply never hears anything.
The fix is one line
Turn the flag off on your own server:
class ExclusiveHTTPServer(http.server.HTTPServer):
allow_reuse_address = False
Now the bind fails the way you wanted it to:
OSError: [WinError 10048] Only one usage of each socket address
(protocol/network address/port) is normally permitted
Which is the message I'd been trying to print by hand.
The asymmetry is what decides whether this can happen to you, and it sits with the second binder rather than the first. If you don't ask for SO_REUSEADDR, your bind gets refused whatever the other process did. If you bind first with allow_reuse_address = False, nobody can quietly join you either. Windows also has SO_EXCLUSIVEADDRUSE for a stronger version of the same guarantee.
The tradeoff you're accepting is the TIME_WAIT annoyance coming back. For a long-running dev server that restarts constantly, keep the default. For anything that must be the listener on a port, turn it off. A short-lived callback catcher is squarely in the second group, and I had it in the first.
What I changed
The catcher binds exclusively now, and it ignores requests that arrive without a code or error parameter, since a browser will also ask for /favicon.ico and that shouldn't count as the callback.
The thing I got wrong wasn't the guess about what could fail. Port already in use was exactly the right failure to plan for. I assumed the operating system would be the one to tell me, and with this class on this platform it stays quiet instead. So if you're binding a well-known port on Windows and then waiting for one specific request, check allow_reuse_address before you trust the startup message.
Top comments (0)