mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 03:00:18 +02:00
Artwork fetches: the deadline bounds the whole request, trickling servers included
Name resolution runs in a thread within the budget, a watchdog shuts the socket at the deadline, and the body is read one receive at a time with the remaining time as timeout. SteamGridDB goes through the same bounded fetch, without redirects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f7bc2a8f68
commit
d7cb967786
3 files changed
+116
-33
No files matched your search
@@ -189,6 +189,57 @@ class ArtworkTests(unittest.TestCase):
|
||||
with self.assertRaises(SourceError):
|
||||
art.fetch('file:///etc/passwd')
|
||||
|
||||
def trickle(self, head, seconds):
|
||||
# A server that answers one byte every 20 ms, over a socketpair standing in for the network.
|
||||
import socket
|
||||
import threading
|
||||
from apk_sources import _images
|
||||
client, server = socket.socketpair()
|
||||
def serve():
|
||||
try:
|
||||
server.recv(65536)
|
||||
for byte in head + b'x' * 1000:
|
||||
server.sendall(bytes([byte]))
|
||||
time.sleep(0.02)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
server.close()
|
||||
threading.Thread(target=serve, daemon=True).start()
|
||||
public = [(2, 1, 6, '', ('93.184.216.34', 80))]
|
||||
with patch.object(_images.socket, 'getaddrinfo', return_value=public), \
|
||||
patch.object(_images.socket, 'create_connection', return_value=client):
|
||||
start = time.monotonic()
|
||||
with self.assertRaisesRegex(_images.SourceError, 'too long'):
|
||||
_images.get('http://example.org/a.png', deadline=start + seconds)
|
||||
return time.monotonic() - start
|
||||
|
||||
def test_deadline_bounds_trickling_headers_and_body(self):
|
||||
self.assertLess(self.trickle(b'HTTP/1.1 200 OK\r\nContent-Length: 1000\r\n\r\n', 0.15), 0.4)
|
||||
self.assertLess(self.trickle(b'HTTP/1.1 200 OK\r\n', 0.15), 0.4) # headers never finish
|
||||
|
||||
def test_deadline_covers_name_resolution(self):
|
||||
import threading
|
||||
from apk_sources import _images
|
||||
gate = threading.Event()
|
||||
with patch.object(_images.socket, 'getaddrinfo', side_effect=lambda *a, **k: gate.wait(5) and []):
|
||||
start = time.monotonic()
|
||||
with self.assertRaisesRegex(_images.SourceError, 'too long'):
|
||||
_images.get('https://example.org/a.png', deadline=start + 0.1)
|
||||
self.assertLess(time.monotonic() - start, 0.4)
|
||||
gate.set()
|
||||
with self.assertRaisesRegex(_images.SourceError, 'too long'):
|
||||
_images.get('https://example.org/a.png', deadline=time.monotonic() - 1)
|
||||
|
||||
def test_steamgriddb_uses_the_bounded_fetch_without_redirects(self):
|
||||
import frame_steamgriddb as sgdb
|
||||
from apk_sources import _images
|
||||
with patch.object(_images, 'get', return_value=b'{"success": true, "data": [1]}') as get:
|
||||
self.assertEqual(sgdb._get('/search/x', 'secret', time.monotonic() + 5), [1])
|
||||
self.assertEqual(get.call_args.kwargs['redirects'], 0)
|
||||
self.assertEqual(get.call_args.args[1]['Authorization'], 'Bearer secret')
|
||||
self.assertLessEqual(get.call_args.kwargs['deadline'] - time.monotonic(), 5)
|
||||
|
||||
def test_supplied_jpeg(self):
|
||||
data = (FIXTURES / 'icon.jpg').read_bytes()
|
||||
self.assertEqual(art.image_type(data), 'jpg')
|
||||
|
||||
Reference in new issue
Block a user