fix(downloads): a download that hangs up mid-transfer is not an error at the egress gate
Seen live: yt-dlp closes a connection once it has read what it needed, and the gate printed a traceback for each. A reset ends the tunnel quietly; anything else the gate cannot finish is a log line with its code, not bare stderr.
This commit is contained in:
@@ -93,6 +93,15 @@ def _read_head(sock: socket.socket) -> bytes | None:
|
||||
|
||||
|
||||
def _pipe(a: socket.socket, b: socket.socket) -> None:
|
||||
try:
|
||||
_relay(a, b)
|
||||
except OSError:
|
||||
# silent-ok: one side hung up mid-transfer (yt-dlp closes a connection once it has read a
|
||||
# file's size, or gives up on a slow host); whichever side cared reports it itself.
|
||||
return
|
||||
|
||||
|
||||
def _relay(a: socket.socket, b: socket.socket) -> None:
|
||||
socks = [a, b]
|
||||
while True:
|
||||
ready, _, _ = select.select(socks, [], [], _IDLE_S)
|
||||
@@ -196,6 +205,11 @@ class _Gate(socketserver.ThreadingTCPServer):
|
||||
def url(self) -> str:
|
||||
return f"http://127.0.0.1:{self.server_address[1]}"
|
||||
|
||||
def handle_error(self, request, client_address) -> None:
|
||||
# socketserver's default prints a bare traceback to stderr, outside the log; a connection the
|
||||
# gate could not finish is a line with its code instead.
|
||||
log.exception("egress gate dropped a connection", extra=logs.event("download.egress.error"))
|
||||
|
||||
|
||||
def start_gate(
|
||||
*, resolve: Resolver = socket.getaddrinfo, forbidden: Forbidden = urlguard.is_forbidden
|
||||
|
||||
@@ -10,7 +10,9 @@ import http.server
|
||||
import ipaddress
|
||||
import socket
|
||||
import socketserver
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -178,6 +180,24 @@ def test_a_tunnel_into_the_network_is_refused_before_it_is_dialled(gate):
|
||||
internal.close()
|
||||
|
||||
|
||||
def test_a_client_that_hangs_up_mid_transfer_is_not_an_error(gate, monkeypatch):
|
||||
# yt-dlp closes a connection once it has read what it needed; seen live as a traceback the gate
|
||||
# printed for every such download.
|
||||
errors_seen = []
|
||||
monkeypatch.setattr(gate, "handle_error", lambda *a: errors_seen.append(a))
|
||||
public = _public_server({"/big": (200, {}, b"x" * 4_000_000)})
|
||||
try:
|
||||
s = socket.create_connection(("127.0.0.1", gate.server_address[1]))
|
||||
s.sendall(f"GET http://{PUBLIC}:{public.server_address[1]}/big HTTP/1.1\r\n\r\n".encode())
|
||||
s.recv(1024)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
||||
s.close() # a reset, not a polite close
|
||||
time.sleep(0.5)
|
||||
assert errors_seen == []
|
||||
finally:
|
||||
public.shutdown()
|
||||
|
||||
|
||||
# --- a real yt-dlp through it ----------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user