improve local server stability

Hunter
Thursday, July 23rd, 2026 at 1:02 PM

https_serve.py+27-4
serve.py+43-28

https_serve.py
301  	return httpd
302  
303  
304 +class TLSThreadingServer(socketserver.ThreadingTCPServer):
305 +	"""Threading server that negotiates TLS in the worker thread, so
306 +	handshakes run in parallel."""
307 +	daemon_threads = True
308 +	allow_reuse_address = True
309 +	# default backlog of 5 drops connections when a device precaches in bursts
310 +	request_queue_size = 128
311 +
312 +	def __init__(self, server_address, handler_cls, ssl_context):
313 +		self.ssl_context = ssl_context
314 +		super().__init__(server_address, handler_cls)
315 +
316 +	def process_request_thread(self, request, client_address):
317 +		try:
318 +			request.settimeout(20)  # cap how long a dead client's handshake lingers
319 +			request = self.ssl_context.wrap_socket(request, server_side=True)
320 +			request.settimeout(None)
321 +		except (ssl.SSLError, OSError):
322 +			# Untrusted CA, a probe, or a client that walked away.
323 +			self.shutdown_request(request)
324 +			return
325 +		super().process_request_thread(request, client_address)
326 +
327 +
328  def serve_https(local_ip, base_path, ca_httpd=None):
329  	"""Serve SCRIPT_DIR over HTTPS for PWA installation, mounted under
330  	base_path so the installed app's start_url resolves. Runs until Ctrl+C,

351  		def handle(self):
352  			try:
353  				super().handle()
    -			except (BrokenPipeError, ConnectionResetError):
354 +			except (BrokenPipeError, ConnectionResetError, ssl.SSLError):
355  				pass
356  
357  		def translate_path(self, path):

365  			if base_path != "/" and self.path in ("/", "/index.html"):
366  				self.send_response(302)
367  				self.send_header("Location", base_path)
368 +				self.send_header("Content-Length", "0")
369  				self.end_headers()
370  				return
371  			# super() is serve.TrackRequestHandler.do_GET, which handles

383  			ca_httpd.server_close()
384  		sys.exit(1)
385  
    -	with socketserver.ThreadingTCPServer(("", port), QuietHandler) as httpd:
    -		httpd.daemon_threads = True
    -		httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
386 +	with TLSThreadingServer(("", port), QuietHandler, ctx) as httpd:
387  		https_url = f"https://{local_ip}:{port}{base_path}"
388  
389  		print("\n" + "=" * 59)
serve.py
222  
223  
224  class TrackRequestHandler(http.server.SimpleHTTPRequestHandler):
225 +	# keep-alive; every response below needs a Content-Length or must close
226 +	protocol_version = 'HTTP/1.1'
227 +	# don't let idle connections pin threads forever
228 +	timeout = 60
229 +	# Nagle would stall the body write behind the header write
230 +	disable_nagle_algorithm = True
231 +
232  	def do_GET(self):
233  		# Rescan /mix on every tracks.json fetch so the page always sees
234  		# what's actually on disk (added via rip.py/buy.py or by hand).

247  		self.send_response(200)
248  		self.send_header('Content-Type', 'text/event-stream')
249  		self.send_header('Cache-Control', 'no-cache')
    -		self.send_header('Connection', 'keep-alive')
250 +		# The stream has no Content-Length, so under HTTP/1.1 it has to be
251 +		# delimited by closing the socket. Sending this also flips
252 +		# close_connection, so the handler won't try to reuse the socket.
253 +		self.send_header('Connection', 'close')
254  		self.send_header('X-Accel-Buffering', 'no')
255  		self.end_headers()
256  

281  			with sse_subscribers_lock:
282  				sse_subscribers.discard(q)
283  
284 +	def _reply_status(self, status, body=b''):
285 +		"""Short reply with a Content-Length. Errors also close the socket,
286 +		since an early return can leave an unread body in the buffer."""
287 +		if isinstance(body, str):
288 +			body = body.encode('utf-8')
289 +		self.send_response(status)
290 +		self.send_header('Content-Length', str(len(body)))
291 +		if status >= 400:
292 +			self.send_header('Connection', 'close')
293 +		self.end_headers()
294 +		if body:
295 +			try:
296 +				self.wfile.write(body)
297 +			except (BrokenPipeError, ConnectionResetError):
298 +				pass
299 +
300  	def _reply_json(self, status, payload):
301  		body = json.dumps(payload).encode('utf-8')
302  		self.send_response(status)

314  	def do_POST(self):
315  		# Local-only: receive a reordered tracks array and overwrite tracks.json
316  		if self.path != '/tracks':
    -			self.send_response(404)
    -			self.end_headers()
317 +			self._reply_status(404)
318  			return
319  		length = int(self.headers.get('Content-Length', '0'))
320  		payload = json.loads(self.rfile.read(length))

333  		# desired insertion index (X-Insert-After: -1 means prepend).
334  		prefix = '/upload/'
335  		if not self.path.startswith(prefix):
    -			self.send_response(404)
    -			self.end_headers()
336 +			self._reply_status(404)
337  			return
338  
339  		raw_name = urllib.parse.unquote(self.path[len(prefix):])

341  		filename = Path(raw_name).name
342  		ext = Path(filename).suffix.lower()
343  		if not filename or ext not in SUPPORTED_UPLOAD_EXTENSIONS:
    -			self.send_response(415)
    -			self.end_headers()
344 +			self._reply_status(415)
345  			return
346  
347  		length = int(self.headers.get('Content-Length', '0'))
348  		if length <= 0 or length > MAX_UPLOAD_BYTES:
    -			self.send_response(413)
    -			self.end_headers()
349 +			self._reply_status(413)
350  			return
351  
352  		try:

369  					remaining -= len(chunk)
370  			if remaining != 0:
371  				tmp_path.unlink(missing_ok=True)
    -				self.send_response(400)
    -				self.end_headers()
372 +				self._reply_status(400)
373  				return
374  
375  			with tracks_lock:

408  			self._reply_json(200, {'filename': new_name, 'final_index': final_index})
409  		except Exception as e:
410  			tmp_path.unlink(missing_ok=True)
    -			self.send_response(500)
    -			self.end_headers()
    -			try:
    -				self.wfile.write(str(e).encode('utf-8'))
    -			except Exception:
    -				pass
411 +			self._reply_status(500, str(e))
412  
413  	def do_DELETE(self):
414  		# Local-only: remove a track's audio file from /mix and prune
415  		# tracks.json. Path is /tracks/<url-encoded-filename>.
416  		prefix = '/tracks/'
417  		if not self.path.startswith(prefix):
    -			self.send_response(404)
    -			self.end_headers()
418 +			self._reply_status(404)
419  			return
420  
421  		filename = urllib.parse.unquote(self.path[len(prefix):])

423  		try:
424  			target.relative_to(MIX_DIR.resolve())
425  		except ValueError:
    -			self.send_response(400)
    -			self.end_headers()
426 +			self._reply_status(400)
427  			return
428  		if target == TRACKS_PATH.resolve() or target.name != filename:
    -			self.send_response(400)
    -			self.end_headers()
429 +			self._reply_status(400)
430  			return
431  		with tracks_lock:
432  			try:

434  			except FileNotFoundError:
435  				pass
436  			except OSError as e:
    -				self.send_response(500)
    -				self.end_headers()
    -				self.wfile.write(str(e).encode('utf-8'))
437 +				self._reply_status(500, str(e))
438  				return
439  			tracks = [t for t in read_tracks() if t.get('filename') != filename]
440  			TRACKS_PATH.write_text(

484  				pass
485  
486  
487 +	class ThreadedServer(socketserver.ThreadingTCPServer):
488 +		daemon_threads = True
489 +		# default backlog of 5 drops connections when a browser opens several at once
490 +		request_queue_size = 128
491 +
492  	try:
    -		with socketserver.ThreadingTCPServer(("", port), QuietHandler) as httpd:
    -			httpd.daemon_threads = True
493 +		with ThreadedServer(("", port), QuietHandler) as httpd:
494  			local_url = f"http://localhost:{port}"
495  			network_url = f"http://{local_ip}:{port}"
496